本文對ThreadLocal弱引用進行一些解析,以及ThreadLocal使用注意事項。java
首先,簡單回顧一下,ThreadLocal是一個線程本地變量,每一個線程維護本身的變量副本,多個線程互相不可見,所以多線程操做該變量沒必要加鎖,適合不一樣線程使用不一樣變量值的場景。數組
其實現原理這裏就不作詳細闡述,其數據結構是每一個線程Thread類都有個屬性ThreadLocalMap,用來維護該線程的多個ThreadLocal變量,該Map是自定義實現的Entry<K,V>[]數組結構,並不是繼承自原生Map類,Entry其中Key便是ThreadLocal變量自己,Value則是具體該線程中的變量副本值。結構如圖:數據結構
所以ThreadLocal其實只是個符號意義,自己不存儲變量,僅僅是用來索引各個線程中的變量副本。多線程
值得注意的是,Entry的Key即ThreadLocal對象是採用弱引用引入的,如源代碼:this
static class ThreadLocalMap { /** * 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; } }
本文下面重點分析爲什麼使用弱引用,以及可能存在的問題。spa
首先看下弱引用。線程
java語言中爲對象的引用分爲了四個級別,分別爲 強引用 、軟引用、弱引用、虛引用。代碼規範
其他三種具體可自行查閱相關資料。code
弱引用具體指的是java.lang.ref.WeakReference<T>類。對象
對對象進行弱引用不會影響垃圾回收器回收該對象,即若是一個對象只有弱引用存在了,則下次GC將會回收掉該對象(無論當前內存空間足夠與否)。
再來講說內存泄漏,假如一個短生命週期的對象被一個長生命週期對象長期持有引用,將會致使該短生命週期對象使用完以後得不到釋放,從而致使內存泄漏。
所以,弱引用的做用就體現出來了,可使用弱引用來引用短生命週期對象,這樣不會對垃圾回收器回收它形成影響,從而防止內存泄漏。
1.爲何ThreadLocalMap使用弱引用存儲ThreadLocal?
假如使用強引用,當ThreadLocal再也不使用須要回收時,發現某個線程中ThreadLocalMap存在該ThreadLocal的強引用,沒法回收,形成內存泄漏。
所以,使用弱引用能夠防止長期存在的線程(一般使用了線程池)致使ThreadLocal沒法回收形成內存泄漏。
2.那一般說的ThreadLocal內存泄漏是如何引發的呢?
咱們注意到Entry對象中,雖然Key(ThreadLocal)是經過弱引用引入的,可是value即變量值自己是經過強引用引入。
這就致使,假如不做任何處理,因爲ThreadLocalMap和線程的生命週期是一致的,當線程資源長期不釋放,即便ThreadLocal自己因爲弱引用機制已經回收掉了,但value仍是駐留在線程的ThreadLocalMap的Entry中。即存在key爲null,但value卻有值的無效Entry。致使內存泄漏。
但實際上,ThreadLocal內部已經爲咱們作了必定的防止內存泄漏的工做。
即以下方法:
/** * Expunge a stale entry by rehashing any possibly colliding entries * lying between staleSlot and the next null slot. This also expunges * any other stale entries encountered before the trailing null. See * Knuth, Section 6.4 * * @param staleSlot index of slot known to have null key * @return the index of the next null slot after staleSlot * (all between staleSlot and this slot will have been checked * for expunging). */ private int expungeStaleEntry(int staleSlot) { Entry[] tab = table; int len = tab.length; // expunge entry at staleSlot tab[staleSlot].value = null; tab[staleSlot] = null; size--; // Rehash until we encounter null Entry e; int i; for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { ThreadLocal<?> k = e.get(); if (k == null) { e.value = null; tab[i] = null; size--; } else { int h = k.threadLocalHashCode & (len - 1); if (h != i) { tab[i] = null; // Unlike Knuth 6.4 Algorithm R, we must scan until // null because multiple entries could have been stale. while (tab[h] != null) h = nextIndex(h, len); tab[h] = e; } } } return i; }
上述方法的做用是擦除某個下標的Entry(置爲null,能夠回收),同時檢測整個Entry[]表中對key爲null的Entry一併擦除,從新調整索引。
該方法,在每次調用ThreadLocal的get、set、remove方法時都會執行,即ThreadLocal內部已經幫咱們作了對key爲null的Entry的清理工做。
可是該工做是有觸發條件的,須要調用相應方法,假如咱們使用完以後不作任何處理是不會觸發的。
目前咱們使用多線程都是經過線程池管理的,對於核心線程數以內的線程都是長期駐留池內的。顯式調用remove,一方面是防止內存泄漏,最爲重要的是,不及時清除有可能致使嚴重的業務邏輯問題,產生線上故障(使用了上次未清除的值)。
最佳實踐:在ThreadLocal使用先後都調用remove清理,同時對異常狀況也要在finally中清理。
在某些代碼規範中遇到過這樣一條要求:「儘可能不要使用全局的ThreadLocal」。關於這點有兩種解讀。最初個人解讀是,由於靜態變量的生命週期和類的生命週期是一致的,而類的卸載時機能夠說比較苛刻,這會致使靜態ThreadLocal沒法被垃圾回收,容易出現內存泄漏。另外一個解讀,我諮詢了編寫該規範的對方解釋是,若是流程中改變了變量值,下次複用該流程可能致使獲取到非預期的值。
但實際上,這兩個解讀都是沒必要要的,首先,靜態ThreadLocal資源回收的問題,即便ThreadLocal自己沒法回收,但線程中的Entry是能夠經過remove清理掉的也就不會出現泄漏。第二種解讀,屢次複用值改變的問題,其實在調用remove後也不會出現。
而若是ThreadLocal不加static,則每次其所在類實例化時,都會有重複ThreadLocal建立。這樣即便線程在訪問時不出現錯誤也有資源浪費。
所以,ThreadLocal通常加static修飾,同時要遵循第一條及時清理。
我的博客:www.hellolvs.cn