上一篇文章中說明了ThreadLocal的使用,部分源碼實現以及Thread,ThreadLocal,ThreadLocalMap三者之間的關聯關係,其中核心實現ThreadLocalMap將在本篇文章中進行講解,讓咱們一塊兒來探究下jdk中的ThreadLocalMap是如何實現的java
JDK版本號:1.8.0_171
在源碼正式解讀以前有些知識須要提早了解的,以便更好的理解源碼實現。以前文章中的關聯關係是理解ThreadLocal內部實現結構的重點,這裏回顧下:程序員
在圖裏咱們能夠看到threadLocals變量就是ThreadLocalMap實現的一個實例對象,每一個線程對應一個ThreadLocalMap,其實現是kv結構,在第一次看到這個名字你們就會想到Map結構吧,而其底層實現與Map是相似的,若是你曾經看過HashMap源碼,能夠回想下HashMap的實現原理,固然也能夠參考我之前對HashMap源碼文章的分析。在這裏你們須要提早去了解下散列表這種數據結構,不然下面的知識理解起來比較困難算法
在註釋上咱們能夠看到做者對其進行了一些解釋:ThreadLocalMap是一個定製的hashmap,僅用於維護線程的本地變量值,只有ThreadLocal有操做權限,是Thread的私有屬性。哈希表中的key是弱引用WeakReference實現,當GC時會進行清理未被引用的entry。有些人可能會比較疑惑,請你們繼續往下閱讀就好,咱們一步一步來理解其內部實現編程
既然都是散列表結構,那麼ThreadLocalMap和HashMap的實現有什麼區別呢?數組
這裏只以jdk8版本源碼進行比較,最重要的區別在於解決hash衝突的方式,在HashMap中使用鏈表法解決衝突,使得其底層數據結構的實現使用了鏈表,固然,爲了提高效率,在達到閾值時轉化爲了紅黑樹。然而,在ThreadLocalMap中並未使用這種方式,而採用了開放尋址法解決衝突,於是並不須要鏈表這種數據結構安全
那麼這兩種解決衝突的方式有什麼不一樣呢?數據結構
這裏借用王爭老師的《數據結構和算法之美》中的結論,你們能夠思考下理解理解:併發
使用開放尋址法解決衝突的散列表,裝載因子的上限不能太大。致使這種方法比鏈表法更浪費內存空間,因此當數據量比較小、裝載因子小的時候,適合採用開放尋址法。
基於鏈表的散列衝突處理方法比較適合存儲大對象、大數據量的散列表,並且,比起開放尋址法,它更加靈活,支持更多的優化策略,好比用紅黑樹代替鏈表
再次提醒下,在源碼的學習中須要先去了解散列表(也就是哈希表)這種數據結構,散列衝突的緣由以及其解決散列衝突的方式,若是未了解過,建議先去學習下,不然下面的講解理解起來相對比較困難。這裏我只說明下開放尋址法中使用線性探測解決衝突的方式,由於這種方式也就是ThreadLocalMap中解決散列衝突的方式,下面經過插入,查找,刪除操做了解其使用,便於後續ThreadLocalMap的源碼實現理解數據結構和算法
在插入數據時若是出現了散列衝突,就從新探測一個空閒位置,將其插入。當咱們往散列表中插入數據時,若是某個數據通過散列函數散列以後,存儲位置已經被佔用了,那麼就從當前位置開始,依次日後查找,直至查找到空閒位置爲止。舉個例子,以下圖,咱們散列表大小爲14,咱們輸入一個數x在通過散列函數hash()散列後應該存放到數組中散列槽(即數組索引)爲2的位置上,可是2已經被佔用了,這樣就形成了散列衝突,那麼咱們就須要依次向後查找空閒位置,最終查找到5的空閒位置,將數x保存在5的位置上函數
散列表元素查找和插入過程相似。當咱們查找時散列到對應的散列槽上,比較對應散列槽上的數據與咱們查找的數據是否相等,相等則表示查找成功了,不等則說明衝突了,咱們須要與插入元素相似的方式依次向後進行查找比較,找到正確的元素便可。若是遍歷到數組中的空閒位置,尚未找到,就說明要查找的元素並無在散列表中。一般比較的是鍵值對中的鍵,就如同map結構
散列表中的元素刪除操做就有些不太同樣了,你能夠想一想,若是咱們僅僅將散列表中對應元素刪除後什麼都不作會有什麼問題?看下查找操做,你就應該明白,若是不處理的話查找操做會被破壞,仍是繼續元素插入的例子,在5的位置已經放了對應的數據x,此時執行刪除操做,將3位置上保存的數據刪除,若是不作任何處理,咱們再次查找散列表中是否存在x,在查找操做進行到3位置上時發現空閒,就會認爲查找的元素不在散列表中,這樣就形成了查找錯誤,由於x明明在5的位置上
因此你應該明白的是,刪除操做須要保證查找操做的正確性,那麼如何解決呢?其中一種就是在刪除元素以後,將以後不爲null的數據rehash操做,這樣就能保證查找的正確性(參考下圖),固然,這也就是ThreadLocalMap刪除操做實現的方式,然而,實際上並不止這一種方式,好比還能夠邏輯刪除,即不是真正刪除,只是打個標識,說明這個數據被刪除了,查詢時發現這個標識就跳過
提醒:請先去學習瞭解下java中的強,軟,弱,虛引用,要不下列知識可能難以理解!
因爲WeakReference在ThreadLocalMap中的使用不得不先來了解下內存泄漏這個知識點,常常有人說ThreadLocal使用時要注意不要內存泄漏,那麼,什麼是內存泄漏?
建立的對象再也不被其餘應用程序使用,但由於被其餘對象所引用着(即經過可達性分析,從GC Roots具備到該對象的鏈路),所以垃圾回收器沒辦法回收它們,此時將在內存中一直被佔用,形成浪費
簡單來講,就是咱們其實已經使用完畢了,可是因爲還具備有效的引用致使GC沒法回收,對象在內存中一直佔用致使浪費,其實這是一個相對的概念,也就是說這些建立的對象是否是咱們想要保留還要使用的,若是不是,那就算內存泄漏,若是是,那就不算。內存泄漏和WeakReference自己是沒有關係的,基本上都是使用不當致使的
ThreadLocalMap中很明顯的部分在於key是弱引用,在key的生命週期完成後發生GC必然會被回收,而value自己就是被Entry強引用,可是並非說必定會形成內存泄漏,若是value只在線程中被定義,初始化,使用,那麼在線程生命週期結束以後,經過可達性算法分析,value一樣會被回收,若是value在線程外被定義,初始化,也就是在線程外經過可達性算法還能夠找到鏈路,那麼即便線程結束,value依舊不會被回收,可是這裏請注意,若是你這裏的value還有用,那麼也不算內存泄漏,只有當這個value無用了沒被回收才能算內存泄漏。也就是說內存泄漏並非ThreadLocalMap的專屬,ThreadLocalMap不背這個鍋,就像使用集合類也有可能形成內存泄漏同樣,最終仍是須要理解並正確的使用才能避免這種情況
那麼什麼狀況下使用ThreadLocalMap會形成內存泄漏呢?最多見的就是在線程池中了,線程池中的線程存活時間基本都是與程序同生共死的,這樣就致使Thread持有的ThreadLocalMap一直都不會被回收,再加上ThreadLocalMap中的Entry對ThreadLocal是弱引用(WeakReference),因此只要 ThreadLocal 結束了本身的生命週期是能夠被回收掉的。可是Entry中的Value倒是被Entry強引用的,因此即使Value的生命週期結束了,Value 也是沒法被回收的,從而致使內存泄露。因此ThreadLocal使用時推薦手動remove操做避免可能出現的內存泄漏風險
Entry繼承WeakReference,ThreadLocal做爲key使用弱引用,在ThreadLocalMap的數組中存放的也就是這個Entry,這裏使用WeakReference也就是不想讓ThreadLocal在生命週期結束後因爲這裏的引用而致使其使用的內存沒法被回收
static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }
與HashMap實現相似,經過數組實現散列表,只不過HashMap使用的是Node鏈表節點,而這裏Entry僅僅保存kv值,不須要next指針,固然,形成這種不一樣結果的緣由就是由於解決hash衝突的方式是不一樣的
/** * 初始化容量,必須爲2的冪,默認16 */ private static final int INITIAL_CAPACITY = 16; /** * table,Entry數組 */ private Entry[] table; /** * table中元素實際個數 */ private int size = 0; /** * 擴容閾值大小,默認爲0 */ private int threshold; // Default to 0
ThreadLocalMap只有當被使用到時纔會進行初始化操做,也就是懶加載模式,其中散列槽位的計算經過ThreadLocal中生成的hashcode和(數組長度-1)進行與操做進行定位,很是方便,若是須要繼承父線程中的ThreadLocalMap變量,則須要使用複製父線程的ThreadLocalMap構造方法,經過ThreadLocal.createInheritedMap方法進行操做
/* 懶加載,只有須要保存鍵值時才初始化 */ ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) { // 初始化table數組 table = new Entry[INITIAL_CAPACITY]; // hashcode和(容量-1)與操做做爲該數據存放到數組中的索引位置 int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); // 在對應的數組索引位置建立Entry table[i] = new Entry(firstKey, firstValue); // 數組中元素數量加1 size = 1; // 設置閾值 setThreshold(INITIAL_CAPACITY); } /* 參數傳入父線程相關的ThreadLocalMap */ private ThreadLocalMap(ThreadLocalMap parentMap) { Entry[] parentTable = parentMap.table; int len = parentTable.length; setThreshold(len); // 把父線程的ThreadLocalMap中的數據複製到當前線程的ThreadLocalMap 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(); // key非空的部分entry if (key != null) { Object value = key.childValue(e.value); // 建立Entry Entry c = new Entry(key, value); // 肯定數組中的索引位置 int h = key.threadLocalHashCode & (len - 1); // 若索引位置已經被使用,則使用開放尋址法解決hash衝突,從新計算索引位置 while (table[h] != null) h = nextIndex(h, len); // 將建立的Entry保存到對應的table數組索引中 table[h] = c; // 數組中實際元素個數加1 size++; } } } }
首先爲了方便講解作個說明,key(也就是ThreadLocal)爲null的,可是value非空的entry這裏稱爲無用entry,並非null值,只是key爲null,也就是被ThreadLocal被回收了的entry
調整閾值時使用數組容量的2/3做爲新的閾值,同時在rehash方法中能夠看到在數組中元素個數達到閾值的3/4,也就是容量的1/2時進行擴容操做,能夠看出這種解決hash衝突的方式仍是比較浪費內存空間的
private void setThreshold(int len) { threshold = len * 2 / 3; }
prevIndex查找索引i的前一個值,若是當前索引爲0,則直接取數組最大索引值len - 1。nextIndex查找索引i的後一個值,若是爲最大索引值len - 1,則直接取0。至關於在一個環形數組中查找i的先後索引,這裏封裝好便於使用
private static int prevIndex(int i, int len) { return ((i - 1 >= 0) ? i - 1 : len - 1); } private static int nextIndex(int i, int len) { return ((i + 1 < len) ? i + 1 : 0); }
獲取ThreadLocal對應的數組table中的Entry對象,因爲其使用開放尋址法解決hash衝突,故在未直接命中時須要進一步的查找處理
private Entry getEntry(ThreadLocal<?> key) { // 獲取散列槽也就是數組的索引位置 int i = key.threadLocalHashCode & (table.length - 1); Entry e = table[i]; if (e != null && e.get() == key) // 命中則直接返回 return e; else // 未命中則經過getEntryAfterMiss繼續查找 return getEntryAfterMiss(key, i, e); }
在首次經過threadLocalHashCode計算索引槽位時,未直接命中,即上面的getEntry方法,則經過此方法進一步繼續查找
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) { Entry[] tab = table; // table容量 int len = tab.length; // e爲null說明數組中無對應的entry,直接返回null結束查找 while (e != null) { // 獲取對應entry的ThreadLocal ThreadLocal<?> k = e.get(); // 數組槽位上的key相等,則命中返回 if (k == key) return e; if (k == null) // 鍵爲null說明ThreadLocal已經被回收 // 這裏進行清除無用entry操做並對其以後的entry進行rehash操做以保證查找的正確性 // 直到遍歷到null的數組槽位中止 expungeStaleEntry(i); else // 非空且key不等則繼續查找下一個索引位置的entry // 也就是繼續向後查找匹配 i = nextIndex(i, len); e = tab[i]; } return null; }
清除ThreadLocal已經被回收的無用entry,同時進行rehash操做,因爲刪除了數組中的entry,爲了保證開放尋址法查找匹配entry的正確性,必然要對刪除元素這個操做進行後續的處理,而這裏是經過對其以後到第一個null元素之間的全部元素進行rehash操做,能夠參考我上面講解的開放尋址法刪除操做的處理。能夠看到expungeStaleEntry是對哈希槽中刪除entry所處的這一段數據進行處理以保證查找的正確性
private int expungeStaleEntry(int staleSlot) { Entry[] tab = table; int len = tab.length; // expunge entry at staleSlot // 刪除無用entry,置空操做,標識gc可回收 tab[staleSlot].value = null; tab[staleSlot] = null; // 記錄減1 size--; // Rehash until we encounter null // 遍歷刪除entry以後的entry // 清除數組中無用的entry // 同時非null時rehash操做,若是衝突繼續使用開放尋址法解決 // 直到碰見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) { // 無用entry處理 e.value = null; tab[i] = null; size--; } else { int h = k.threadLocalHashCode & (len - 1); // 非i位置表示須要從新設置該entry所處的槽位 // 也就是進行rehash操做 if (h != i) { // 先將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); // entry賦值 tab[h] = e; } } } return i; }
以ThreadLocal爲key構建Entry添加到table對應的散列槽上,在ThreadLocal.set和ThreadLocal.setInitialValue中被調用
private void set(ThreadLocal<?> key, Object value) { Entry[] tab = table; int len = tab.length; // 定位散列槽位 int i = key.threadLocalHashCode & (len-1); // hash衝突使用開放尋址法解決 for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { ThreadLocal<?> k = e.get(); // 對應key相同,更新value if (k == key) { e.value = value; return; } // ThreadLocal被回收,可是這個槽位上的entry還在 // 能夠理解爲使用當前的新的key和value取代無用的entry // 可是須要注意replaceStaleEntry內部處理並無這麼簡單,可參考下面的方法說明 if (k == null) { replaceStaleEntry(key, value, i); return; } } // 執行到這裏說明循環中未處理,i處爲null,沒有entry佔用,可被使用 tab[i] = new Entry(key, value); int sz = ++size; // 嘗試清理部分無效entry,可參考下面的方法解釋 if (!cleanSomeSlots(i, sz) && sz >= threshold) // 若是未清理任何數據同時使用長度已經達到閾值限制則執行rehash方法 rehash(); }
使用入參取代無用的hash槽位上的entry,即staleSlot上的key已經被回收了,可是entry還在佔用,使用key和value來進行替換。同時會經過expungeStaleEntry清理這一段數組數據,cleanSomeSlots來嘗試檢查清理整個數組中一些無用的entry
這裏有個很是關鍵的地方須要理解,這個方法是在set中被調用處理碰見無用entry的操做,咱們須要保存新的鍵值對,同時對無用entry進行處理,清理無用entry有個重要的地方在於,從哪一個點開始進行處理,回想上面的expungeStaleEntry方法,咱們在清除無用的entry時還會對其後的部分entry進行rehash遷移操做,那麼咱們就須要找到當前無用entry這一段數組中第一個無用entry所在的位置,有點繞人,也不是很難理解,首先,這一段數組也就是在這個staleSlot位置的無用entry所在的,向前查找第一個非null的entry和向後查找第一個非null的entry,咱們須要處理這段數組中無用的entry,而expungeStaleEntry是從無用entry向後進行處理的,因此咱們須要找到這段數組中第一個無用entry所在的位置,參考下圖理解下
圖中3,5,8的位置爲無用的entry,此時執行replaceStaleEntry操做的是5的位置,那麼最終會從3的位置執行expungeStaleEntry對這一段數組數據進行處理,其餘狀況相似,你能夠本身畫圖理解,這裏就不一一進行說明了
private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) { Entry[] tab = table; int len = tab.length; Entry e; // 待清除的槽位 // 這裏實際上是找到這段非null數組中第一個無用entry的位置 int slotToExpunge = staleSlot; // 遍歷staleSlot前的槽位上的entry,找到其以前一個槽位非null,key爲null的entry // 固然是最靠近null的那個無用entry的位置 for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len)) if (e.get() == null) slotToExpunge = i; // 遍歷staleSlot以後的非null的entry // 這裏和向前遍歷處理不同 for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { ThreadLocal<?> k = e.get(); // 對應槽位上的entry的key相等,則替換value if (k == key) { e.value = value; // 與staleSlot位置交換 // 由於staleSlot位置是無用的entry,直接替換掉,這樣再次散列時其能夠直接從staleSlot位置獲取了 tab[i] = tab[staleSlot]; tab[staleSlot] = e; // 若是上面的向前遍歷未找到無用entry則根據這裏的i處已經被更換爲key爲null的entry,從i處開始清理 // 若是向前遍歷已經找到無用entry則不更新slotToExpunge,從向前遍歷獲得的slotToExpunge開始處理 if (slotToExpunge == staleSlot) slotToExpunge = i; cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); return; } // 當前entry爲無用entry同時向前遍歷未找到無用entry則更新這裏向後遍歷中得到的第一個無用entry的位置 // 最後的代碼會從這個位置開始清理 if (k == null && slotToExpunge == staleSlot) slotToExpunge = i; } // 未找到匹配的key則直接替換掉staleSlot位置的entry便可 tab[staleSlot].value = null; tab[staleSlot] = new Entry(key, value); // 不相等則說明其餘位置有無用的entry 須要進行清理操做 if (slotToExpunge != staleSlot) cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); }
掃描數組中部分槽位,清理無用entry,經過n >>>= 1
控制掃描次數,若是找到無用entry則重置掃描次數,若是未掃描到無用entry則每次調用掃描次數爲log(n)
。cleanSomeSlots是嘗試查找部分無用entry並進行清理操做,爲了下降掃描次數,並不一一進行檢查操做
private boolean cleanSomeSlots(int i, int n) { boolean removed = false; Entry[] tab = table; int len = tab.length; do { i = nextIndex(i, len); Entry e = tab[i]; // 掃描到無用entry if (e != null && e.get() == null) { // 重置n n = len; removed = true; // 調用expungeStaleEntry處理 i = expungeStaleEntry(i); } } while ( (n >>>= 1) != 0); return removed; }
移除對應key的entry操做,同時經過expungeStaleEntry進行刪除操做的後續處理
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) { // 找到匹配entry則進行清理 // clear清理的是key,先變爲無用entry再使用expungeStaleEntry清理 e.clear(); expungeStaleEntry(i); return; } } }
對整個table哈希槽進行rehash操做,同時判斷是否須要進行擴容操做
private void rehash() { // 清理table中全部的無用entry expungeStaleEntries(); // 判斷是否進行擴容操做 if (size >= threshold - threshold / 4) resize(); }
經過expungeStaleEntry清理table中的全部無用entry
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); } }
擴容操做,固定擴容爲原數組容量的2倍,這裏遷移entry也比較暴力,直接計算對應槽位,而後使用開放尋址法解決衝突,將entry放入對應的新數組中的槽位上
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) { // ThreadLocal爲空則value也置空便於GC回收 e.value = null; // Help the GC } else { // 在新table中設置到對應的槽位上 int h = k.threadLocalHashCode & (newLen - 1); while (newTab[h] != null) h = nextIndex(h, newLen); newTab[h] = e; count++; } } } // 設置閾值 setThreshold(newLen); size = count; table = newTab; }
爲何Thread中的變量定義是ThreadLocalMap而不是ThreadLocal?
借用在《Java併發編程實戰》中的話能夠解釋下這個緣由:
在Java的實現方案裏面,ThreadLocal僅僅是一個代理工具類,內部並不持有任何與線程相關的數據,全部和線程相關的數據都存儲在Thread裏面,這樣的設計容易理解。而從數據的親緣性上來說,ThreadLocalMap屬於Thread也更加合理。固然還有一個更加深層次的緣由,那就是不容易產生內存泄露。假如ThreadLocal持有的Map會持有Thread對象的引用,這就意味着,只要ThreadLocal對象存在,那麼Map中的 Thread對象就永遠不會被回收。ThreadLocal的生命週期每每都比線程要長,因此這種設計方案很容易致使內存泄露。而Java的實現中Thread持有ThreadLocalMap,並且ThreadLocalMap裏對ThreadLocal 的引用仍是弱引用(WeakReference),因此只要 Thread 對象能夠被回收,那麼ThreadLocalMap就能被回收。Java的這種實現方案雖然看上去複雜一些,可是更加安全。Java 的ThreadLocal實現應該稱得上深思熟慮了,不過即使如此深思熟慮,仍是不能百分百地讓程序員避免內存泄露,例如在線程池中使用 ThreadLocal,若是不謹慎就可能致使內存泄露。
到此關於ThreadLocal的源碼基本講解完畢,整體來講有些地方仍是比較難理解的,你能夠多思考思考,相信有不同的收穫
本文講解了Hash衝突的解決方式之一開放尋址法是爲了幫助你們更好的理解源碼實現,同時說明了內存泄漏的風險,而後對源碼部分進行了詳細的說明,正確理解其內部數據結構是理解源碼實現的關鍵,同時結合上篇文章,理清Thread,ThreadLocal,ThreadLocalMap三者之間的關聯關係,相信對於ThreadLocal的使用便會了然於心。對於實現中最重要的在於經過expungeStaleEntry,cleanSomeSlots,expungeStaleEntries完成無用entry的清理,同時進行數據的rehash操做便於開放尋址法查找數據的正確性,相信對你們而言不是過於複雜
以上內容若有問題歡迎指出,筆者驗證後將及時修正,謝謝