要回答ThreadLocalMap裡弱引用,我們需要弄清者三個問題
第一個問題,我們先來看看引用相關的。其實Java中一直有争論關于值傳遞與引用傳遞(就我看到的百度是這樣的)。我們先來看看代碼。
public static void main(String[] args){
//測試引用傳遞
Map<String,String> a = new HashMap<>();
a.put("1", "a");
List<Map> b = new ArrayList<Map>();
b.add(a);
a = null;
Map aa = b.get(0);
System.out.println(aa.get("1"));
//結論傳遞的是引用的副本
//測試值傳遞
int xx = 1;
add(xx);
System.out.print(xx);
}
public static void add(int temp){
temp++;
}
最後結果是
a
1
從這裡我們看到其實傳遞的都是副本,隻不過基本類型傳遞的是值的副本,而引用傳遞的是引用的副本。
由此可見引用傳遞隻是把自己指向某個對象的指向複制了一份給了形參。并且當操作完add()方法後,list中存留的也是複制後的引用,是以當 a = null 後list中的引用也能正确的指向到對象。
是以我們可以了解為有兩份引用了,也就是導緻當我們執行 a = null 後GC不會回收開始a 所指向的對象,因為還有一份存在了list中。
第二個問題,是關于強、軟、弱、虛引用的,各個意思我就不解釋了,具體的千萬别百度。
我就簡單解釋其中需要用到的弱引用。
弱引用 WeakReference
如果一個對象隻具有弱引用,那麼垃圾回收器在掃描到該對象時,無論記憶體充足與否,都會回收該對象的記憶體。
第三個問題,最後的問題了,重頭戲了。
Java裡,每個線程都有自己的ThreadLocalMap,裡邊存着自己私有的對象。Map的Entry裡,key為ThreadLocal對象,value即為私有對象T。在spring MVC中,常用ThreadLocal儲存目前登陸使用者資訊,這樣線程在任意地方都可以取到使用者資訊了。
每個
Thread
内部都維護一個
ThreadLocalMap
字典資料結構,字典的Key值是
ThreadLocal
,那麼當某個
ThreadLocal
對象不再使用(沒有其它地方再引用)時,每個已經關聯了此
ThreadLocal
的線程怎麼在其内部的
ThreadLocalMap
裡做清除此資源呢?JDK中的
ThreadLocalMap
又做了一次精彩的表演,它沒有繼承
java.util.Map
類,而是自己實作了一套專門用來定時清理無效資源的字典結構。其内部存儲實體結構
Entry<ThreadLocal, T>
繼承自
java.lan.ref.WeakReference
,這樣當
ThreadLocal
不再被引用時,因為弱引用機制原因,當jvm發現記憶體不足時,會自動回收弱引用指向的執行個體記憶體,即其線程内部的
ThreadLocalMap
會釋放其對
ThreadLocal
的引用進而讓jvm回收
ThreadLocal
對象。這裡是重點強調下,是回收對
ThreadLocal
對象,而非整個
Entry
,是以線程變量中的值
T
對象還是在記憶體中存在的,是以記憶體洩漏的問題還沒有完全解決。接着分析JDK的實作,會發現在調用
ThreadLocal.get()
或者
ThreadLocal.set(T)
時都會定期執行回收無效的
Entry
操作。
可以看個簡單的例子
public class UserContext {
private static final ThreadLocal<UserInfo> userInfoLocal = new ThreadLocal<UserInfo>();
public static UserInfo getUserInfo() {
return userInfoLocal.get();
}
public static void setUserInfo(UserInfo userInfo) {
userInfoLocal.set(userInfo);
}
public static void clear() {
userInfoLocal.remove();
}
}
我們可以看下源碼,看它哪裡用了弱引用
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a ThreadLocal object).
* Note that null keys (i.e. entry.get() == null) mean that the key is no longer referenced,
* so the entry can be expunged from table.
* Such entries are referred to as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
Entry中的key是弱引用,key 弱指向ThreadLocal<UserInfo> 對象,并且Key隻是userInfoLocal強引用的副本(結合第一個問題),value是userInfo對象。
當我顯示的把userInfoLocal = null 時就隻剩下了key這一個弱引用,GC時也就會回收掉ThreadLocal<UserInfo> 對象。
但是我們最好避免threadLocal=null的操作,盡量用threadLocal.remove()來清除。因為前者中的userInfo對象還是存在強引用在目前線程中,隻有目前thread結束以後, current thread就不會存在棧中,強引用斷開, 會被GC回收。但是如果用的是線程池,那麼的話線程就不會結束,隻會放線上程池中等待下一個任務,但是這個線程的 map 還是沒有被回收,它裡面存在value的強引用,是以會導緻記憶體溢出。
1、在ThreadLocal的生命周期中,都存在這些引用。看下圖: 實線代表強引用,虛線代表弱引用。
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsICM38CXlZHbvN3cpR2Lc1TPB10QGtWUCpEMJ9CXsxWam9CXwADNvwVZ6l2c052bm9CXUJDT1wkNhVzLcRnbvZ2Lc1TPRFGe5ITWwp1MMBjVtJWd0ckW65UbM5WOHJWa5kHT20ESjBjUIF2LcRHelR3LcJzLctmch1mclRXY39zMxUjNwETN5ATOxQDM4EDMy8CX0Vmbu4GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.jpg)
2、ThreadLocal的實作是這樣的:每個Thread 維護一個
ThreadLocalMap
映射表,這個映射表的 key 是
ThreadLocal
執行個體本身,value 是真正需要存儲的 Object。
3、也就是說
ThreadLocal
本身并不存儲值,它隻是作為一個 key 來讓線程從
ThreadLocalMap
擷取 value。值得注意的是圖中的虛線,表示
ThreadLocalMap
是使用
ThreadLocal
的弱引用作為 Key 的,弱引用的對象在 GC 時會被回收。
4、ThreadLocalMap使用ThreadLocal的弱引用作為key,如果一個ThreadLocal沒有外部強引用來引用它,那麼系統 GC 的時候,這個ThreadLocal勢必會被回收,這樣一來,ThreadLocalMap中就會出現key為null的Entry,就沒有辦法通路這些key為null的Entry的value,如果目前線程再遲遲不結束的話,這些key為null的Entry的value就會一直存在一條強引用鍊:
Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value
永遠無法回收,造成記憶體洩漏。
5、總的來說就是,ThreadLocal裡面使用了一個存在弱引用的map, map的類型是
ThreadLocal.ThreadLocalMap.
Map中的key為一個threadlocal執行個體。這個Map的确使用了弱引用,不過弱引用隻是針對key。每個key都弱引用指向threadlocal。 當把threadlocal執行個體置為null以後,沒有任何強引用指向threadlocal執行個體,是以threadlocal将會被gc回收。
但是,我們的value卻不能回收,而這塊value永遠不會被通路到了,是以存在着記憶體洩露。因為存在一條從
current thread
連接配接過來的強引用。隻有目前thread結束以後,
current thread
就不會存在棧中,強引用斷開,Current Thread、Map value将全部被GC回收。最好的做法是将調用threadlocal的remove方法,這也是等會後邊要說的。
6、其實,ThreadLocalMap的設計中已經考慮到這種情況,也加上了一些防護措施:在ThreadLocal的
get(),set(),remove()
的時候都會清除線程ThreadLocalMap裡所有key為null的value。這一點在上一節中也講到過!
7、但是這些被動的預防措施并不能保證不會記憶體洩漏:
(1)使用static的ThreadLocal,延長了ThreadLocal的生命周期,可能導緻記憶體洩漏。
(2)配置設定使用了ThreadLocal又不再調用get(),set(),remove()方法,那麼就會導緻記憶體洩漏,因為這塊記憶體一直存在。