ThreadLocal
類以前有了解過,看過一些文章,自覺得對其理解得比較清楚了。偶然刷到了一道關於ThreadLocal
內存泄漏的面試題,竟然徹底不知道是怎麼回事,痛定思痛,發現瞭解問題的本質仍是須要從源碼看起。
ThreadLocal
能夠保存一些線程私有的數據,從而避免多線程環境下的數據共享問題。ThreadLocal
存儲數據的功能是經過ThreadLocalMap
實現的,這是ThreadLocal
的一個靜態內部類。ThreadLocal
源碼加註釋總共700
多行,ThreadLocalMap
就佔據了接近400
行,基本上理解了ThreadLocalMap
也就理解了ThreadLocal
。本文先簡介ThreadLocalMap
,而後從ThreadLocal
的核心方法開始講起,須要用到ThreadLocalMap
的地方順帶一塊兒介紹。面試
ThreadLocalMap
本質上仍然是一個Map
,具備普通Map
的特色,當遇到hash
衝突的時候,採用線性探測的方式來解決衝突,底層使用數組做爲存儲結構,它的主要字段以下:數組
INITIAL_CAPACITY
:初始容量,默認是16
Entry[] table
:存儲鍵值對的數組,其大小是2
的整數冪size
:數組內存儲的元素個數threshold
:擴容閾值ThreadLocalMap
底層數組保存的是Entry
類型鍵值對,Entry
是ThreadLocalMap
的一個內部類,它是用來存儲鍵值對的對象,值得關注的是Entry
繼承了WeakReference
這個弱引用類,這意味着Entry的key
引用的對象,在沒有其餘強引用的狀況下,在下一次GC的時候就會被回收(注意:這裏忽略了軟引用,由於軟引用是在即將由於內存不足而拋出異常的時候纔會回收)。而且Entry
的key
是ThreadLocal
對象,經過其祖父類Reference
的構造函數能夠看到,key
其實是被保存在referent
字段中,Entry
對象的get
方法也是從Reference
繼承過來的,直接返回該referent
字段。static class Entry extends WeakReference<ThreadLocal<?>> { Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } //Entry的構造器調用了父類的構造,最終是經過Reference的構造器實現的 Reference(T referent) { this(referent, null); } Reference(T referent, ReferenceQueue<? super T> queue) { this.referent = referent; this.queue = (queue == null) ? ReferenceQueue.NULL : queue; } //Reference的get方法 public T get() { return this.referent; }
在對Entry
有了初步瞭解後,如今來思考一下爲何key
要設計成弱引用呢?假設如今採有強引用來設計key
,考慮以下代碼:多線程
ThreadLocal<String> tl1 = new ThreadLocal<>(); tl1.set("abc"); tl1=null;
此時,相關的引用狀況以下圖:
函數
tl1
雖然再也不引用堆上的ThreadLocal
對象,可是線程的ThreadLocalMap
裏還保留着對該對象的強引用,要獲取該對象就須要ThreadLocal
對象做爲key
,可是這個key
如今已是null
了。也就是說,此時已經沒有任何辦法可以訪問到堆上的TheradLocal
對象,可是因爲還有強引用的存在,致使這個對象沒法被GC
回收。這種狀況顯然不是咱們但願看到的,所以Entry
的key
不能被設計爲強引用。設計成弱引用是合理的,一旦外界的強引用被取消,就應當容許key
所引用的對象被回收。this
get
get
方法用來獲取存儲在ThreadLocal
中的元素,其源碼以下:public T get() { Thread t = Thread.currentThread(); //獲取當前線程內部的ThreadLocalMap對象 ThreadLocalMap map = getMap(t); if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } //執行到這裏的兩種狀況:1)map沒初始化;2)map.getEntry返回null return setInitialValue(); } //從這裏能夠看到,每一個Thread示例內部都有一個ThreadLocalMap類型的字段,線程局部變量就存在這個Map中 ThreadLocalMap getMap(Thread t) { return t.threadLocals; } //ThreadLocalMap的getEntry方法 private Entry getEntry(ThreadLocal<?> key) { //計算key位於哪一個桶 int i = key.threadLocalHashCode & (table.length - 1); Entry e = table[i]; if (e != null && e.get() == key) return e; //執行到這裏的兩種狀況:1)e=null,即桶內沒有存數據;2)桶內有數據, //但不是當前這個ThreadLocal對象的,說明產生了hash衝突,致使鍵值對被放到了其餘位置 else return getEntryAfterMiss(key, i, e); }
線程可以保存私有變量的緣由就在於其成員變量threadLocals
,每一個線程都有這樣的結構,互相不干擾。get
方法的代碼很簡單,根據從線程內取到的ThreadLocalMap
對象,若是ThreadLocalMap
還沒初始化,則先初始化;若是已完成初始化,調用其getEntry
方法取元素,取不到的話,就會執行getEntryAfterMiss
方法(ThreadLocal
內部只在getEntry
方法裏調用了getEntryAfterMiss
),先看看setInitialValue
方法的邏輯:線程
private T setInitialValue() { T value = initialValue(); Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); //map已經初始化,就將鍵值對存入底層數組 if (map != null) map.set(this, value); else createMap(t, value); return value; } //默認的initialValue返回null,並且該方法是protected,目的顯然是讓子類進行重寫 protected T initialValue() { return null; }
setInitialValue
的邏輯很簡單,假如map
沒有初始化,執行createMap
方法進行初始化,不然將當前ThreadLocal
對象和null
構形成一個新的Entry
放入數組內。接下來看一下createMap
的初始化邏輯:設計
//能夠看到,初始化的過程就是對Thread內部變量threadLocals賦值的過程,用到了ThreadLocalMap的構造器 void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); } // ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) { //使用默認容量 table = new Entry[INITIAL_CAPACITY]; //計算位置,並初始化對應的桶 int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); table[i] = new Entry(firstKey, firstValue); size = 1; setThreshold(INITIAL_CAPACITY); } //ThreadLocalMap的擴容使用的是2/3做爲加載因子,這點與HashMap等容器不一樣 private void setThreshold(int len) { threshold = len * 2 / 3; }
該方法經過ThreadLocalMap
的構造器對內部數組進行初始化,並將對應的值添加到數組中。能夠看到,ThreadLocalMap
有容量的概念,但卻沒有辦法指定其初始容量,在構造的時候使用固定值16
做爲初始容量,並且擴容閾值設置的是容量的2/3
,這一點與HashMap
等容器的作法不一樣。
接下來看看getEntryAfterMiss
方法的源碼:指針
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) { Entry[] tab = table; int len = tab.length; while (e != null) { ThreadLocal<?> k = e.get(); //找到key就直接返回 if (k == key) return e; //注意這裏:當發現key=null是時,說明其對應的ThreadLocal對象已被GC回收, //此時會經過expungeStaleEntry將一部分key爲null的桶清空 if (k == null) expungeStaleEntry(i); //走到這裏說明存在hash衝突,當前桶被其餘元素佔了,使用nextIndex向後找一個位置 else i = nextIndex(i, len); e = tab[i]; } //若是e=null,在這裏返回null return null; } /nextIndex的主要做用是:查找下一個桶,若是到達末尾,則從頭開始 private static int nextIndex(int i, int len) { return ((i + 1 < len) ? i + 1 : 0); }
在桶內的key=null
時,會調用expungeStaleEntry
方法,從命名能夠看出,這個方法主要功能是將ThreadLocalMap
內key=null
的元素清理掉。下面是對這個方法的講解:code
expungeStaleEntry
private int expungeStaleEntry(int staleSlot) { Entry[] tab = table; int len = tab.length; // expunge entry at staleSlot //分別將Entry的鍵和值清空 tab[staleSlot].value = null; tab[staleSlot] = null; //元素數量減1 size--; // Rehash until we encounter null Entry e; int i; //從當前下標的下一個位置開始遍歷,清空key=null的桶,並更新hash衝突的元素的位置 //循環終止條件:順序向後遍歷時,找到一個非空的桶則循環終止,所以這裏只是做了局部清理 for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { ThreadLocal<?> k = e.get(); //若是key = null,則清空該桶 if (k == null) { e.value = null; tab[i] = null; size--; } else { int h = k.threadLocalHashCode & (len - 1); //h!=i說明當前元素是由於hash衝突,以前的桶被佔了才放在了i這個桶內, //那麼就從其原來的位置h開始向後查找,找到第一個空桶,就把元素挪過去, //目的是爲了保證元素距離其正確位置最近,減小後續的查找成本 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; }
expungeStaleEntry
的循環邏輯說明,在對失效元素進行清空時,不是清空全部失效的桶,而是從當前位置向後遍歷,只要找到一個非空的桶,清理的過程就結束了。也就是說,這種清理會致使有一些過時失效的桶沒法獲得清理。對象
set
get
方法後,如今再來看看set
方法的實現邏輯:public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); //map已建立則直接設值 if (map != null) map.set(this, value); //map未建立,則建立map else createMap(t, value); }
來看看map
的set
方法是如何設值的:
private void set(ThreadLocal<?> key, Object value) { // We don't use a fast path as with get() because it is at // least as common to use set() to create new entries as // it is to replace existing ones, in which case, a fast // path would fail more often than not. Entry[] tab = table; int len = tab.length; int i = key.threadLocalHashCode & (len-1); for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { ThreadLocal<?> k = e.get(); //若是當前ThreadLocal對應的有值,則更新 if (k == key) { e.value = value; return; } //若是k=null,說明對應的ThreadLocal對象已被GC回收,執行replaceStaleEntry的邏輯 if (k == null) { //這裏是replaceStaleEntry方法的惟一調用點 replaceStaleEntry(key, value, i); return; } } //找到一個空位置,將值存進去 tab[i] = new Entry(key, value); int sz = ++size; //若是調用cleanSomeSlots沒有清理任何桶,而且達到了擴容閾值,就執行擴容邏輯 if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }
set
的時候須要從對應的下標開始向後遍歷,找到一個合適的位置將元素放進去,這裏合適的位置是指:a)空桶;b)桶非空,可是key
對應的ThreadLocal
對象已被清理。在key
已經被清理的狀況下,會執行replaceStaleEntry
方法的邏輯,接下來看看這個方法的代碼:
private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) { Entry[] tab = table; int len = tab.length; Entry e; // Back up to check for prior stale entry in current run. // We clean out whole runs at a time to avoid continual // incremental rehashing due to garbage collector freeing // up refs in bunches (i.e., whenever the collector runs). int slotToExpunge = staleSlot; //從staleSlot這個桶向前查找,遇到第一個空桶就中止 for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len)) //若是桶內的key=null,說明該桶能夠被回收,將slotToExpunge變量指向這個桶 if (e.get() == null) slotToExpunge = i; // Find either the key or trailing null slot of run, whichever // occurs first //從當前桶向後遍歷 for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { ThreadLocal<?> k = e.get(); // If we find key, then we need to swap it // with the stale entry to maintain hash table order. // The newly stale slot, or any other stale slot // encountered above it, can then be sent to expungeStaleEntry // to remove or rehash all of the other entries in run. // k== key,說明這個ThreadLocal已經存在,可是距離正確的位置太遠,須要對其位置進行更正 if (k == key) { e.value = value; //交換兩個桶內的元素,把i位置的元素放在距離其正確位置最近的桶內。 //注意,replaceStaleEntry的惟一調用點出如今set方法內,此時staleSlot對應的桶的key=null, //我的推測這裏不直接賦值tab[i]=null的緣由是讓下一次expungeStaleEntry可以多清理一些空桶, //若是這裏設置爲null的話,下一次清理到這個位置就終止了 tab[i] = tab[staleSlot]; tab[staleSlot] = e; // Start expunge at preceding stale entry if it exists //下面的等式成立有兩種狀況: //1)staleSlot前一個桶就爲空,此時上文中前向遍歷的循環體會直接結束; //2)staleSlot前面的若干桶都不爲空,且桶內的key!=null,即對應的ThreadLocal對象都沒有被回收; //出現這兩種狀況的時候,都只能從i這個位置開始進行清理 if (slotToExpunge == staleSlot) slotToExpunge = i; cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); return; } // If we didn't find stale entry on backward scan, the // first stale entry seen while scanning for key is the // first still present in the run. //k!=null的時候,不能清理i這個桶;slotToExpunge != staleSlot時, //說明在i這個位置以前就已經有須要清理的桶了,不能更新slotToExpunge這個指針的值 if (k == null && slotToExpunge == staleSlot) slotToExpunge = i; } // If key not found, put new entry in stale slot //執行到這裏說明,直到遇到空桶,都沒有在數組中找到key,就把新的鍵值對放在staleSlot的位置。 tab[staleSlot].value = null; tab[staleSlot] = new Entry(key, value); // If there are any other stale entries in run, expunge them //slotToExpunge != staleSlot說明在其餘位置找到須要清理的鍵值對,那麼就從對應的位置清理 if (slotToExpunge != staleSlot) cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); }
針對代碼註釋中的內容,有幾點須要強調一下。首先replaceStaleEntry
這個方法惟一的調用點就是ThreadLocalMap
的set
方法內部,並且在調用replaceStaleEntry
的時候,參數staleSlot
對應的桶內的Entry
對象的key=null
。其次,replaceStaleEntry
的邏輯是從staleSlot
這個桶開始,先前向遍歷,找到第一個空桶就中止遍歷,期間若是發現某個桶內的key=null
,就將slotToExpunge
指針指向這個桶,表示下文要從這個桶開始進行過時鍵清理。前向遍歷結束以後開始後向遍歷,找到當前的ThreadLocal
對象所在的桶,將其位置更新,調用清理方法以後代碼返回,不然就一直向後找直到遇到空桶。
下面對replaceStaleEntry方法
的執行流程進行梳理。
假設在方法執行時,ThreadLocalMap
的存儲結構以下所示:
首先前向遍歷,遍歷到LL
位置結束,因爲在L
位置Entry.key=null
,因此設置slotToExpunge=L
;
接下來開始向後遍歷,遍歷到R1
位置時,雖然Entry.key=null
,可是因爲slotToExpunge
的值已經被修改,再也不對其進行賦值。代碼接着遍歷R2
位置,在這裏找到了key
,所以將該位置的值與staleSlot
位置進行交換,以下圖:
以後執行expungeStaleEntry
方法將LL
位置清空,而後從L
位置開始執行cleanSomeSlots
的邏輯。
replaceStaleEntry
方法內有兩處用到了cleanSomeSlots
方法,接下來對其進行介紹:
private boolean cleanSomeSlots(int i, int n) { boolean removed = false; Entry[] tab = table; int len = tab.length; //循環的邏輯是:從i的下一個位置開始找那些桶不空,可是桶內Entry對應的key=null的元素, //而後從這些元素開始向後進行清空。循環的過程當中會跳過空桶或者桶內元素的key!=null的桶, //循環的次數由n的大小決定,每次將n減半,直到減爲0循環結束。 do { i = nextIndex(i, len); Entry e = tab[i]; if (e != null && e.get() == null) { n = len; removed = true; //注意:expungeStaleEntry方法的返回值是第一個不爲空的桶的下標,循環的下一次會從這個下標開始遍歷 i = expungeStaleEntry(i); } } while ( (n >>>= 1) != 0); //若是清理了元素,則返回true,不然返回false return removed; }
rehash
set
方法內部最後幾行,若是調用cleanSomeSlots
沒有清理任何桶,而且達到了擴容閾值,就執行擴容邏輯,這段邏輯在rehash
方法中,來看看方法的實現邏輯:private void rehash() { //清理全部的過時桶 expungeStaleEntries(); // Use lower threshold for doubling to avoid hysteresis //清理事後剩餘元素達到threshold的0.75才進行擴容,回憶一下ThreadLocalMap的初始化過程, //初始化時threshold=2/3*初始容量,這裏在判斷是否要擴容時,是已threshold*0.75爲標準 if (size >= threshold - threshold / 4) resize(); } //這個方法會從頭開始遍歷整個數組,每遇到一個ThreadLocal對象被回收的桶,就調用expungeStaleEntry方法向後清理一部分桶 private void expungeStaleEntries() { Entry[] tab = table; int len = tab.length; for (int j = 0; j < len; j++) { Entry e = tab[j]; if (e != null && e.get() == null) expungeStaleEntry(j); } } private void resize() { Entry[] oldTab = table; int oldLen = oldTab.length; //注意這裏並無對newLen做限制,也就是說有超限的可能,可是通常確定不會在線程內放這麼多本地變量 int newLen = oldLen * 2; Entry[] newTab = new Entry[newLen]; int count = 0; //將原來的數據遷移到新的數組 for (int j = 0; j < oldLen; ++j) { Entry e = oldTab[j]; if (e != null) { ThreadLocal<?> k = e.get(); if (k == null) { e.value = null; // Help the GC } else { int h = k.threadLocalHashCode & (newLen - 1); //找到空位置放入元素 while (newTab[h] != null) h = nextIndex(h, newLen); newTab[h] = e; count++; } } } setThreshold(newLen); size = count; table = newTab; }
remove
ThreadLocal
的remove
方法,方法很簡單,底層邏輯仍然是經過ThreadLocalMap
的remove
實現的:public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) m.remove(this); } private void remove(ThreadLocal<?> key) { Entry[] tab = table; int len = tab.length; //計算key的位置 int i = key.threadLocalHashCode & (len-1); for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { //若是找到key,則調用Reference類的clear方法,將referent置爲null,而後從該位置開始向後清理一部分過時鍵值對 if (e.get() == key) { e.clear(); expungeStaleEntry(i); return; } } } //Reference類的clear方法 public void clear() { this.referent = null; }
ThreadLocal
的底層Entry
數組的key
是弱引用,意味着當ThreadLocal
對象的全部強引用都被去除後,對應的ThreadLocal
對象會被回收,此時Entry
的key=null
,可是value
還維持着堆上數據的強引用,只要當前線程不退出,這個強引用會一直存在。爲了儘量緩解這個問題,ThreadLocal
的get
、set
、remove
方法都會清除一批過時數據,可是從本文的分析能夠看出,這種清理只是部分清理,仍然可能遺漏掉部分數據。所以這三個方法只能在必定程度上緩解內存泄漏的問題,並不能避免。另外,若是線程在較長時間內都沒有執行上述方法,那過時的數據只會更多。那些在一段時間內都沒被清理的過時value
對象仍會繼續佔用內存空間,這些未被清理的對象就是內存泄漏的源頭。固然,過時的數據會在線程退出後所有銷燬,可是當使用了線程池以後,線程用完會重複利用,並不會被銷燬,這種內存泄漏問題就不得不考慮了。所以,好習慣是在ThreadLocal
對象用完以後及時使用remove
方法進行刪除,從而避免內存泄漏問題。