你以為你懂了,其實你沒有! ——寫給自己
一直以來認為ThreadLocal只是簡單的分裝了一下HashMap,使用線程作為key來存儲。這樣也符合我們的習慣思維。需要存儲多少線程變量就創建多少ThreadLocal。
及如下圖這樣:

package gy.mao; import java.util.HashMap; import java.util.Map; public class MyThreadLocal<T> { Map<Thread, T> map = null; public MyThreadLocal() { } public void set(T value) { if (map == null) { map = new HashMap<>(); } map.put(Thread.currentThread(), value); } public T get(){ if (map == null) { throw new RuntimeException("還未初始化"); } return map.get(Thread.currentThread()); } public int size(){ if (map == null) { throw new RuntimeException("還未初始化"); } return map.size(); } }
及通過對HashMap實現簡單的封裝就直接使用;
然而今天仔細的看了看卻發現他實際是這樣的

問題一、為什么實際的ThreadLocal卻不是我們通常所理解的這樣呢?
這就涉及到我們常說的內存泄露的問題。
通過簡單封裝HashMap的方式,如果不手動remove()線程所放入的Entry。那么由於ThreadLocal一直存在,導致該Entry將一直持有,不會被垃圾回收器回收。隨着線程不斷的創建,put數據,必然會內存溢出。
問題二、可是不都說使用的ThreadLocal也是需要remove() 才行,要不然也會內存溢出啊
在回答這個問題前,先做個測試
首先是使用我自己實現的MyThreadLocal測試


然后是使用原生的ThreadLocal測試


兩個程序實現一樣的功能,都是不斷的創建線程然后往ThreadLocal中存數據。
可以很明顯的觀察到,使用我自己實現的MyThreadLocal最終會導致內存溢出,並且手動觸發垃圾回收也無濟於事;這也很符合我們的預期。
可是使用原生的ThreadLocal及時在沒有使用remove的情況下,也沒有內存溢出,而且垃圾回收還很規律的在進行。
問題三、ThreadLocal不使用remove()會有內存泄露的問題?
一起來簡單看下源碼,set()方法,
首先是調用了getMap方法及通過當前線程去獲取ThreadLocalMap(再深入可以看到該ThreadLocalMap實際是屬於當前線程所有),
沒有則創建,有則直接set值進去(注意此時的set的值為this,及當前ThreadLocal)。

再來看下ThreaLocalMap,可以很明顯的看到里面的Entry繼承了弱引用

及ThreadLocal的真正的實現是這樣的(圖片來源於網絡)


因此可以看出,真正的ThreadLocal的哲學為:一個線程維護一個ThreaLocalMap,多個線程變量ThreadLocal存放在這個ThreadLocalMap中,ThreadLocal以弱引用的方式作為ThreadLocalMap的key。
所謂的不remove()會導致內存溢出是: 由於ThreadLocalMap的Entry中key是弱引用,當ThreadLocal不再被使用及會被垃圾回收器回收,但是value 由於是被Entry強引用,而Entry又是被ThreadLocalMap強引用,而ThreadLocalMap又是屬於當前線程的。這樣就形成key已經被回收了,但是Value一直不能回收,且不會再次使用到。
問題四、前面的實驗中使用ThreadLocal沒有導致內存泄露的原因是?
仔細看代碼就知道,代碼中每次都是創建線程之后就往ThreadLocal中放數據,而放完數據之后線程馬上就結束了。因此對應的ThreadLocal,依附於Thread的ThreadLocalMap都是可以被垃圾回收的,及皮將不存,毛之焉附。而不remove會到導致內存泄露是線程還在繼續運行的情況。
問題五、為啥ThreadLocalMap的key是弱引用而不是正常的強引用呢?
如果ThreadLocalMap的key為強引用,那么回收ThreadLocal時,因為ThreadLocalMap還持有ThreadLocal的強引用,ThreadLocal不會被回收,造成浪費,從某種意義上來說也是一種內存泄露。
此外,ThreadLocal沒有采用我鎖實現的MyThreadLocal這種實現思想的一個重要原因是鎖的問題。
眾所周知HashMap有線程安全的問題,如果要使用MyThreadLocal的這種思想必然得考慮該問題,得使用鎖來控制,這無疑增加了性能損耗和復雜度。
而反觀系統提供的ThreadLocal,由於是一個線程維護一個ThreadLocalMap數據,從設計上已經避免了多線程帶來的安全問題。
總結
由於Thread中包含變量ThreadLocalMap,因此ThreadLocalMap與Thread的生命周期是一樣長,如果都沒有手動刪除對應key,都會導致內存泄漏。
但是使用弱引用可以多一層保障:弱引用ThreadLocal不會內存泄漏,對應的value在下一次ThreadLocalMap調用set(),get(),remove()的時候會被清除。
因此,ThreadLocal內存泄漏的根源是:由於ThreadLocalMap的生命周期跟Thread一樣長,如果沒有手動刪除對應key就會導致內存泄漏,而不是因為弱引用。
ThreadLocal正確的使用方法
- 每次使用完ThreadLocal都調用它的remove()方法清除數據
- 將ThreadLocal變量定義成private static,這樣就一直存在ThreadLocal的強引用,也就能保證任何時候都能通過ThreadLocal的弱引用訪問到Entry的value值,進而清除掉 。
參考:https://zhuanlan.zhihu.com/p/102571059
