ThreadLocal源碼解讀,網上面早已經氾濫了,大多比較淺,甚至有的連基本原理都說的頗有問題,包括百度搜索出來的第一篇高訪問量博文,說ThreadLocal內部有個map,鍵爲線程對象,太誤導人了。java
ThreadLocal很是適合對Java多線程編程感興趣的程序員做爲入門類閱讀,緣由兩方面:程序員
本文重點導讀ThreadLocal中的嵌套內部類ThreadLocalMap,對ThreadLocal自己API的介紹簡略帶過。
讀ThreadLocal源碼,不讀ThreadLocalMap的實現,和沒看過沒多大差異。web
先回答兩個問題:算法
什麼是ThreadLocal?
ThreadLocal類顧名思義能夠理解爲線程本地變量。也就是說若是定義了一個ThreadLocal,每一個線程往這個ThreadLocal中讀寫是線程隔離,互相之間不會影響的。它提供了一種將可變數據經過每一個線程有本身的獨立副本從而實現線程封閉的機制。編程
它大體的實現思路是怎樣的?
Thread類有一個類型爲ThreadLocal.ThreadLocalMap的實例變量threadLocals,也就是說每一個線程有一個本身的ThreadLocalMap。ThreadLocalMap有本身的獨立實現,能夠簡單地將它的key視做ThreadLocal,value爲代碼中放入的值(實際上key並非ThreadLocal自己,而是它的一個弱引用)。每一個線程在往某個ThreadLocal裏塞值的時候,都會往本身的ThreadLocalMap裏存,讀也是以某個ThreadLocal做爲引用,在本身的map裏找對應的key,從而實現了線程隔離。api
ThreadLocal的API其實沒多少好介紹的,這些API介紹網上早已爛大街了。
數組
ThreadLocalMap的源碼實現,纔是咱們讀ThreadLocal源碼真正要領悟的。看看大師Doug Lea和Joshua Bloch的鬼斧神工之做。
tomcat
ThreadLocalMap提供了一種爲ThreadLocal定製的高效實現,而且自帶一種基於弱引用的垃圾清理機制。
下面從基本結構開始一點點解讀。安全
既然是個map(注意不要與java.util.map混爲一談,這裏指的是概念上的map),固然得要有本身的key和value,上面回答的問題2中也已經說起,咱們能夠將其簡單視做key爲ThreadLocal,value爲實際放入的值。之因此說是簡單視做,由於實際上ThreadLocal中存放的是ThreadLocal的弱引用。咱們來看看ThreadLocalMap裏的節點是如何定義的。多線程
static class Entry extends WeakReference<java.lang.ThreadLocal<?>> { // 往ThreadLocal裏實際塞入的值 Object value; Entry(java.lang.ThreadLocal<?> k, Object v) { super(k); value = v; } }
Entry即是ThreadLocalMap裏定義的節點,它繼承了WeakReference類,定義了一個類型爲Object的value,用於存放塞到ThreadLocal裏的值。
讀到這裏,若是不問不答爲何是這樣的定義形式,爲何要用弱引用,等於沒讀懂源碼。
由於若是這裏使用普通的key-value形式來定義存儲結構,實質上就會形成節點的生命週期與線程強綁定,只要線程沒有銷燬,那麼節點在GC分析中一直處於可達狀態,沒辦法被回收,而程序自己也沒法判斷是否能夠清理節點。弱引用是Java中四檔引用的第三檔,比軟引用更加弱一些,若是一個對象沒有強引用鏈可達,那麼通常活不過下一次GC。當某個ThreadLocal已經沒有強引用可達,則隨着它被垃圾回收,在ThreadLocalMap裏對應的Entry的鍵值會失效,這爲ThreadLocalMap自己的垃圾清理提供了便利。
/** * 初始容量,必須爲2的冪 */ private static final int INITIAL_CAPACITY = 16; /** * Entry表,大小必須爲2的冪 */ private Entry[] table; /** * 表裏entry的個數 */ private int size = 0; /** * 從新分配表大小的閾值,默認爲0 */ private int threshold;
能夠看到,ThreadLocalMap維護了一個Entry表或者說Entry數組,而且要求表的大小必須爲2的冪,同時記錄表裏面entry的個數以及下一次須要擴容的閾值。
顯然這裏會產生一個問題,爲何必須是2的冪?很好,可是目前還沒法回答,帶着問題接着往下讀。
/** * 設置resize閾值以維持最壞2/3的裝載因子 */ private void setThreshold(int len) { threshold = len * 2 / 3; } /** * 環形意義的下一個索引 */ private static int nextIndex(int i, int len) { return ((i + 1 < len) ? i + 1 : 0); } /** * 環形意義的上一個索引 */ private static int prevIndex(int i, int len) { return ((i - 1 >= 0) ? i - 1 : len - 1); }
ThreadLocal須要維持一個最壞2/3的負載因子,對於負載因子相信應該不會陌生,在HashMap中就有這個概念。
ThreadLocal有兩個方法用於獲得上一個/下一個索引,注意這裏其實是環形意義下的上一個與下一個。
因爲ThreadLocalMap使用線性探測法來解決散列衝突,因此實際上Entry[]數組在程序邏輯上是做爲一個環形存在的。
關於開放尋址、線性探測等內容,能夠參考網上資料或者TAOCP(《計算機程序設計藝術》)第三卷的6.4章節。
至此,咱們已經能夠大體勾勒出ThreadLocalMap的內部存儲結構。下面是我繪製的示意圖。虛線表示弱引用,實線表示強引用。
ThreadLocalMap維護了Entry環形數組,數組中元素Entry的邏輯上的key爲某個ThreadLocal對象(其實是指向該ThreadLocal對象的弱引用),value爲代碼中該線程往該ThreadLoacl變量實際塞入的值。
好的,接下來再來看看ThreadLocalMap的一個構造函數
/** * 構造一個包含firstKey和firstValue的map。 * ThreadLocalMap是惰性構造的,因此只有當至少要往裏面放一個元素的時候纔會構建它。 */ ThreadLocalMap(java.lang.ThreadLocal<?> firstKey, Object firstValue) { // 初始化table數組 table = new Entry[INITIAL_CAPACITY]; // 用firstKey的threadLocalHashCode與初始大小16取模獲得哈希值 int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); // 初始化該節點 table[i] = new Entry(firstKey, firstValue); // 設置節點表大小爲1 size = 1; // 設定擴容閾值 setThreshold(INITIAL_CAPACITY); }
這個構造函數在set和get的時候均可能會被間接調用以初始化線程的ThreadLocalMap。
重點看一下上面構造函數中的int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
這一行代碼。
ThreadLocal類中有一個被final修飾的類型爲int的threadLocalHashCode,它在該ThreadLocal被構造的時候就會生成,至關於一個ThreadLocal的ID,而它的值來源於
/* * 生成hash code間隙爲這個魔數,可讓生成出來的值或者說ThreadLocal的ID較爲均勻地分佈在2的冪大小的數組中。 */ private static final int HASH_INCREMENT = 0x61c88647; private static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT); }
能夠看出,它是在上一個被構造出的ThreadLocal的ID/threadLocalHashCode的基礎上加上一個魔數0x61c88647的。這個魔數的選取與斐波那契散列有關,0x61c88647對應的十進制爲1640531527。斐波那契散列的乘數能夠用(long) ((1L << 31) * (Math.sqrt(5) - 1))能夠獲得2654435769,若是把這個值給轉爲帶符號的int,則會獲得-1640531527。換句話說
(1L << 32) - (long) ((1L << 31) * (Math.sqrt(5) - 1))
獲得的結果就是1640531527也就是0x61c88647。經過理論與實踐,當咱們用0x61c88647做爲魔數累加爲每一個ThreadLocal分配各自的ID也就是threadLocalHashCode再與2的冪取模,獲得的結果分佈很均勻。
ThreadLocalMap使用的是線性探測法,均勻分佈的好處在於很快就能探測到下一個臨近的可用slot,從而保證效率。這就回答了上文拋出的爲何大小要爲2的冪的問題。爲了優化效率。
對於& (INITIAL_CAPACITY - 1)
,相信有過算法競賽經驗或是閱讀源碼較多的程序員,一看就明白,對於2的冪做爲模數取模,能夠用&(2^n-1)來替代%2^n,位運算比取模效率高不少。至於爲何,由於對2^n取模,只要不是低n位對結果的貢獻顯然都是0,會影響結果的只能是低n位。
能夠說在ThreadLocalMap中,形如key.threadLocalHashCode & (table.length - 1)
(其中key爲一個ThreadLocal實例)這樣的代碼片斷實質上就是在求一個ThreadLocal實例的哈希值,只是在源碼實現中沒有將其抽爲一個公用函數。
這個方法會被ThreadLocal的get方法直接調用,用於獲取map中某個ThreadLocal存放的值。
private Entry getEntry(ThreadLocal<?> key) { // 根據key這個ThreadLocal的ID來獲取索引,也即哈希值 int i = key.threadLocalHashCode & (table.length - 1); Entry e = table[i]; // 對應的entry存在且未失效且弱引用指向的ThreadLocal就是key,則命中返回 if (e != null && e.get() == key) { return e; } else { // 由於用的是線性探測,因此日後找仍是有可能可以找到目標Entry的。 return getEntryAfterMiss(key, i, e); } } /* * 調用getEntry未直接命中的時候調用此方法 */ private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) { Entry[] tab = table; int len = tab.length; // 基於線性探測法不斷向後探測直到遇到空entry。 while (e != null) { ThreadLocal<?> k = e.get(); // 找到目標 if (k == key) { return e; } if (k == null) { // 該entry對應的ThreadLocal已經被回收,調用expungeStaleEntry來清理無效的entry expungeStaleEntry(i); } else { // 環形意義下日後面走 i = nextIndex(i, len); } e = tab[i]; } return null; } /** * 這個函數是ThreadLocal中核心清理函數,它作的事情很簡單: * 就是從staleSlot開始遍歷,將無效(弱引用指向對象被回收)清理,即對應entry中的value置爲null,將指向這個entry的table[i]置爲null,直到掃到空entry。 * 另外,在過程當中還會對非空的entry做rehash。 * 能夠說這個函數的做用就是從staleSlot開始清理連續段中的slot(斷開強引用,rehash slot等) */ private int expungeStaleEntry(int staleSlot) { Entry[] tab = table; int len = tab.length; // 由於entry對應的ThreadLocal已經被回收,value設爲null,顯式斷開強引用 tab[staleSlot].value = null; // 顯式設置該entry爲null,以便垃圾回收 tab[staleSlot] = null; size--; Entry e; int i; for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { ThreadLocal<?> k = e.get(); // 清理對應ThreadLocal已經被回收的entry if (k == null) { e.value = null; tab[i] = null; size--; } else { /* * 對於尚未被回收的狀況,須要作一次rehash。 * * 若是對應的ThreadLocal的ID對len取模出來的索引h不爲當前位置i, * 則從h向後線性探測到第一個空的slot,把當前的entry給挪過去。 */ 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. * * 這段話說起了Knuth高德納的著做TAOCP(《計算機程序設計藝術》)的6.4章節(散列) * 中的R算法。R算法描述瞭如何從使用線性探測的散列表中刪除一個元素。 * R算法維護了一個上次刪除元素的index,當在非空連續段中掃到某個entry的哈希值取模後的索引 * 尚未遍歷到時,會將該entry挪到index那個位置,並更新當前位置爲新的index, * 繼續向後掃描直到遇到空的entry。 * * ThreadLocalMap由於使用了弱引用,因此其實每一個slot的狀態有三種也即 * 有效(value未回收),無效(value已回收),空(entry==null)。 * 正是由於ThreadLocalMap的entry有三種狀態,因此不能徹底套高德納原書的R算法。 * * 由於expungeStaleEntry函數在掃描過程當中還會對無效slot清理將之轉爲空slot, * 若是直接套用R算法,可能會出現具備相同哈希值的entry之間斷開(中間有空entry)。 */ while (tab[h] != null) { h = nextIndex(h, len); } tab[h] = e; } } } // 返回staleSlot以後第一個空的slot索引 return i; }
咱們來回顧一下從ThreadLocal讀一個值可能遇到的狀況:
根據入參threadLocal的threadLocalHashCode對錶容量取模獲得index
private void set(ThreadLocal<?> key, Object value) { 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(); // 找到對應的entry if (k == key) { e.value = value; return; } // 替換失效的entry if (k == null) { replaceStaleEntry(key, value, i); return; } } tab[i] = new Entry(key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) { rehash(); } } private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) { Entry[] tab = table; int len = tab.length; Entry e; // 向前掃描,查找最前的一個無效slot int slotToExpunge = staleSlot; for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len)) { if (e.get() == null) { slotToExpunge = i; } } // 向後遍歷table for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { ThreadLocal<?> k = e.get(); // 找到了key,將其與無效的slot交換 if (k == key) { // 更新對應slot的value值 e.value = value; tab[i] = tab[staleSlot]; tab[staleSlot] = e; /* * 若是在整個掃描過程當中(包括函數一開始的向前掃描與i以前的向後掃描) * 找到了以前的無效slot則以那個位置做爲清理的起點, * 不然則以當前的i做爲清理起點 */ if (slotToExpunge == staleSlot) { slotToExpunge = i; } // 從slotToExpunge開始作一次連續段的清理,再作一次啓發式清理 cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); return; } // 若是當前的slot已經無效,而且向前掃描過程當中沒有無效slot,則更新slotToExpunge爲當前位置 if (k == null && slotToExpunge == staleSlot) { slotToExpunge = i; } } // 若是key在table中不存在,則在原地放一個便可 tab[staleSlot].value = null; tab[staleSlot] = new Entry(key, value); // 在探測過程當中若是發現任何無效slot,則作一次清理(連續段清理+啓發式清理) if (slotToExpunge != staleSlot) { cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); } } /** * 啓發式地清理slot, * i對應entry是非無效(指向的ThreadLocal沒被回收,或者entry自己爲空) * n是用於控制控制掃描次數的 * 正常狀況下若是log n次掃描沒有發現無效slot,函數就結束了 * 可是若是發現了無效的slot,將n置爲table的長度len,作一次連續段的清理 * 再從下一個空的slot開始繼續掃描 * * 這個函數有兩處地方會被調用,一處是插入的時候可能會被調用,另外個是在替換無效slot的時候可能會被調用, * 區別是前者傳入的n爲元素個數,後者爲table的容量 */ private boolean cleanSomeSlots(int i, int n) { boolean removed = false; Entry[] tab = table; int len = tab.length; do { // i在任何狀況下本身都不會是一個無效slot,因此從下一個開始判斷 i = nextIndex(i, len); Entry e = tab[i]; if (e != null && e.get() == null) { // 擴大掃描控制因子 n = len; removed = true; // 清理一個連續段 i = expungeStaleEntry(i); } } while ((n >>>= 1) != 0); return removed; } private void rehash() { // 作一次全量清理 expungeStaleEntries(); /* * 由於作了一次清理,因此size極可能會變小。 * ThreadLocalMap這裏的實現是調低閾值來判斷是否須要擴容, * threshold默認爲len*2/3,因此這裏的threshold - threshold / 4至關於len/2 */ if (size >= threshold - threshold / 4) { resize(); } } /* * 作一次全量清理 */ 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) { /* * 我的以爲這裏能夠取返回值,若是大於j的話取了用,這樣也是可行的。 * 由於expungeStaleEntry執行過程當中是把連續段內全部無效slot都清理了一遍了。 */ expungeStaleEntry(j); } } } /** * 擴容,由於須要保證table的容量len爲2的冪,因此擴容即擴大2倍 */ private void resize() { Entry[] oldTab = table; int oldLen = oldTab.length; 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; } else { // 線性探測來存放Entry int h = k.threadLocalHashCode & (newLen - 1); while (newTab[h] != null) { h = nextIndex(h, newLen); } newTab[h] = e; count++; } } } setThreshold(newLen); size = count; table = newTab; }
咱們來回顧一下ThreadLocal的set方法可能會有的狀況
/** * 從map中刪除ThreadLocal */ private void remove(ThreadLocal<?> key) { 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)]) { if (e.get() == key) { // 顯式斷開弱引用 e.clear(); // 進行段清理 expungeStaleEntry(i); return; } } }
remove方法相對於getEntry和set方法比較簡單,直接在table中找key,若是找到了,把弱引用斷了作一次段清理。
關於ThreadLocal是否會引發內存泄漏也是一個比較有爭議性的問題,其實就是要看對內存泄漏的準肯定義是什麼。
認爲ThreadLocal會引發內存泄漏的說法是由於若是一個ThreadLocal對象被回收了,咱們往裏面放的value對於【當前線程->當前線程的threadLocals(ThreadLocal.ThreadLocalMap對象)->Entry數組->某個entry.value】這樣一條強引用鏈是可達的,所以value不會被回收。
認爲ThreadLocal不會引發內存泄漏的說法是由於ThreadLocal.ThreadLocalMap源碼實現中自帶一套自我清理的機制。
之因此有關於內存泄露的討論是由於在有線程複用如線程池的場景中,一個線程的壽命很長,大對象長期不被回收影響系統運行效率與安全。若是線程不會複用,用完即銷燬了也不會有ThreadLocal引起內存泄露的問題。《Effective Java》一書中的第6條對這種內存泄露稱爲unintentional object retention
(無心識的對象保留)。
當咱們仔細讀過ThreadLocalMap的源碼,咱們能夠推斷,若是在使用的ThreadLocal的過程當中,顯式地進行remove是個很好的編碼習慣,這樣是不會引發內存泄漏。
那麼若是沒有顯式地進行remove呢?只能說若是對應線程以後調用ThreadLocal的get和set方法都有很高的機率會順便清理掉無效對象,斷開value強引用,從而大對象被收集器回收。
但不管如何,咱們應該考慮到什麼時候調用ThreadLocal的remove方法。一個比較熟悉的場景就是對於一個請求一個線程的server如tomcat,在代碼中對web api做一個切面,存放一些如用戶名等用戶信息,在鏈接點方法結束後,再顯式調用remove。
對於InheritableThreadLocal,本文不做過多介紹,只是簡單略過。
ThreadLocal自己是線程隔離的,InheritableThreadLocal提供了一種父子線程之間的數據共享機制。
它的具體實現是在Thread類中除了threadLocals外還有一個inheritableThreadLocals
對象。
在線程對象初始化的時候,會調用ThreadLocal的createInheritedMap
從父線程的inheritableThreadLocals
中把有效的entry都拷過來
能夠看一下其中的具體實現
private ThreadLocalMap(ThreadLocalMap parentMap) { Entry[] parentTable = parentMap.table; int len = parentTable.length; setThreshold(len); table = new Entry[len]; for (int j = 0; j < len; j++) { Entry e = parentTable[j]; if (e != null) { @SuppressWarnings("unchecked") ThreadLocal<Object> key = (ThreadLocal<Object>) e.get(); if (key != null) { // 這裏的childValue方法在InheritableThreadLocal中默認實現爲返回自己值,能夠被重寫 Object value = key.childValue(e.value); Entry c = new Entry(key, value); int h = key.threadLocalHashCode & (len - 1); while (table[h] != null) h = nextIndex(h, len); table[h] = c; size++; } } } }
仍是比較簡單的,作的事情就是以父線程的inheritableThreadLocalMap
爲數據源,過濾出有效的entry,初始化到本身的inheritableThreadLocalMap
中。其中childValue能夠被重寫。
須要注意的地方是InheritableThreadLocal
只是在子線程建立的時候會去拷一份父線程的inheritableThreadLocals
。若是父線程是在子線程建立後再set某個InheritableThreadLocal對象的值,對子線程是不可見的。
本博文重點介紹了ThreadLocal中ThreadLocalMap的大體實現原理以及ThreadLocal內存泄露的問題以及簡略介紹InheritableThreadLocal。做爲Josh Bloch和Doug Lea兩位大師之做,ThreadLocal自己實現的算法與技巧仍是很優雅的。在開發過程當中,ThreadLocal用到恰到好處的話,能夠消除一些代碼的重複。但也要注意過分使用ThreadLocal很容易加大類之間的耦合度與依賴關係(開發過程可能會不得不過分考慮某個ThreadLocal在調用時是否已有值,存放的是哪一個類放的什麼值)。