天天看點

一文帶你讀懂JDK源碼:ThreadLocal類

線程封閉是實作線程安全的手段之一(另外的線程安全手段還有:使用并發工具類,可以參考)。實作線程封閉的方法,就是今天的主角 -- ThreadLocal 類了;下面我們從4個角度剖析 ThreadLocal 類的源碼:應用場景&功能、底層資料結構&源碼、記憶體洩漏&規避手段 和 replaceStaleEntry()方法講解。

winter

另外一個基礎内容是:ThreadLocal 哈希沖突的解決方法使用到了開放位址法,此處可以結合 HashMap 原理進行區分了解(HashMap 是通過鍊位址法解決hash 沖突問題,即:數組+連結清單+紅黑樹)。

(1)開放位址法是什麼?

基本思想:當發生位址沖突時,按照某種方法繼續探測哈希表中的其他存儲單元,直到找到空位置為止。

缺點:

容易産生堆積問題,不适于大規模的資料存儲。

散列函數的設計對沖突會有很大的影響,插入時可能會出現多次沖突的現象。

删除的元素是多個沖突元素中的一個,需要對後面的元素作處理,實作較複雜。

(2)ThreadLocalMap 采用開放位址法原因是什麼?ThreadLocal 源碼有一個屬性HASH_INCREMENT = 0x61c88647 ,0x61c88647 是一個神奇的數字,容量長度 capacity 和 HASH_INCREMENT 與運算,得到的數組位址索引 index 往往能更加均勻的分布在2的N次方的數組裡。由于ThreadLocal 往往存放的資料量注定不會特别大(而且key 是弱引用又會被垃圾回收,及時讓資料量更小),這個時候開放位址法簡單的結構會顯得更省空間,同時數組的查詢效率也是非常高,加上第一點的保障,沖突機率也低。

一文帶你讀懂JDK源碼:ThreadLocal類
一文帶你讀懂JDK源碼:ThreadLocal類

應用場景&功能

ThreadLocal 的作用是提供線程内的局部變量,這種變量線上程的生命周期内起作用,減少同一個線程内多個函數或者元件之間一些公共變量的傳遞的複雜度。

下面我們通過一個例子,來了解 ThreadLocal 的使用:

在多線程環境下,ThreadLocal 變量被多線程同時通路并使用,驗證它是否是線程安全的。

輸出結果:

代碼分析:

例子1一共出現了 3 個線程:主線程、子線程1和子線程2,一共出現了 2 個  ThreadLocal 對象;這三個線程都分别對 ThreadLocal 對象進行了指派,然後分别在自己的工作空間(堆棧)列印了 ThreadLocal 所指派的内容。

我們發現,雖然 三個線程都出現了對 ThreadLocal 相同對象的使用,但是最終列印結果都比對上了線程内部操作結果,是以驗證了“ThreadLocal 可以實作資源的線程封閉”。

ThreadLocal中填充的變量屬于目前線程,該變量對其他線程而言是隔離的。ThreadLocal為變量在每個線程中都建立了一個副本,那麼每個線程可以通路自己内部的副本變量。

一文帶你讀懂JDK源碼:ThreadLocal類
一文帶你讀懂JDK源碼:ThreadLocal類

底層資料結構&源碼

底層資料結構

ThreadLocal 比較特殊,它的内部并沒有像 HashMap 等工具類那樣自行維護一個存儲資料的容器,而是提供了一個内部類定義給 Thread 類進行初始化引用,這個内部類就是 ThreadLocalMap 類。

是以我們剖析 ThreadLocal 底層,就是結合 Thread 類去了解 ThreadLocalMap 這個内部類所提供的能力(而這個内部類同樣内部嵌套了另外一個内部類,那就是 Entry 類,不着急,慢慢來)。

我們先看看 ThreadLocalMap 内部類的源碼:

分析源碼:

構造器(自不用多說)

嵌套内部類 Entry (它是一個弱引用,可以參考文章《JDK提供的四種引用類型》)

為何使用Entry[]來維護每個數值,而不是使用HashMap這樣鍵值對來存儲資料?因為HashMap都是強引用,難以被清GC理,回收效率低。

維護了一個數組容器:Entry[] table,它的每個元素類型大都是 嵌套内部類 Entry 對象;

數組容器的初始化長度:INITIAL_CAPACITY = 16;

我們開頭提到“ ThreadLocal 隻是提供了一個靜态内部類 ThreadLocalMap 給 Thread 類進行初始化引用”,是以我們需要了解下Thread的源碼:

    分析源碼:

我們得出結論:每個線程内部單獨維護了一個 ThreadLocal.ThreadLocalMap 的引用,而這個 ThreadLocal.ThreadLocalMap 内部核心是 Entry[] 數組,它是實作線程封閉的核心;

那麼這個Entry數組到底如何實作線程隔離的呢?且繼續看下面的“常用API源碼”小節。

 小結:整理得到 ThreadLocal 的底層資料結構

一文帶你讀懂JDK源碼:ThreadLocal類

一言蔽之,Thread 線程維護了一個 ThreadLocalMap,而 ThreadLocalMap 内部就是一個 Entry數組,該數組裝的是 ThreadLocal 類對象和它所持有的值對象。

常用API源碼

ThreadLocal 的三個最常用API,前面兩個是 set(T value) 與 get(),分别實作指派與擷取值,remove()則是清理值。

一文帶你讀懂JDK源碼:ThreadLocal類

set(T value) 

一文帶你讀懂JDK源碼:ThreadLocal類

我們看下源碼的 set(T value) 方法源碼:

源碼的第一步:擷取目前操作線程,并通過getMap(Thread t)拿到線程内部的成員變量 ThreadLocalMap ;

源碼的第二步:判空Map是否存在,存在好辦設定值完事,不存在就建立再設定值,此時Map的鍵值對為:key=目前ThreadLocal對象,value=傳入值;

那麼重點來了,此處的設定值相當考究,我們繼續研究這兩個代碼段:

(1)根據createMap() 方法,我們定位到 ThreadLocalMap 的 構造器:

createMap:就幹一件事情,給目前線程的 ThreadLocalMap 成員變量初始化。

第一步,初始化 ThreadLocalMap 内部維護的 Entry[] 數組,長度設定為 16;

第二步,确定Entry 下标政策 = “目前ThreadLocalMap對象.hashCode 和 Entry[] 數組長度的 按位與運算結果”;

第三步, 将(目前ThreadLocalMap對象,設定值對象)封裝為Entry元素,并塞到數組中;

第四步,設定 size 屬性(長度) 以及 threshold 屬性(擴容門檻值);

(2)根據map.set() 方法,我們定位到 ThreadLocalMap 的 set() 代碼塊:

第一步,計算數組下标索引(政策跟上文說的一樣)

第二步,周遊 Entry 數組,此處的循環成立條件為 e != null(元素沖突);

這裡面臨兩種可能:

其中一種是沖突 Entry 的弱引用 ThreadLocal 還沒被GC清理掉(執行指派操作);

另一種是沖突 Entry 的弱引用 ThreadLocal被GC清理掉了(執行 replaceStaleEntry 操作,這部分源碼有點複雜,我們單獨拎出來講解);

第三步,周遊完 Entry 數組,都沒有發現有元素下标沖突的(執行指派操作);

一文帶你讀懂JDK源碼:ThreadLocal類

get()

一文帶你讀懂JDK源碼:ThreadLocal類

我們看下源碼的 get() 方法源碼: 

源碼分析:

第一步,擷取目前線程的 ThreadLocalMap 成員變量

第二步,ThreadLocalMap 裡面尋址到 Key = ThreadLocal,直接傳回;

第三步,如果 ThreadLocalMap 尋址失敗,則執行 setInitialValue() 方法塊;

一文帶你讀懂JDK源碼:ThreadLocal類

remove()

一文帶你讀懂JDK源碼:ThreadLocal類

我們看下源碼的 remove() 方法源碼:

跟以上兩個API比較,這部分内容相對簡單,直接清理掉 ThreadLocalMap 的 Entry[] 數組的命中元素;這個操作直接導緻一個結果,那就是 ThreadLocal 對象的引用鍊會斷開,這個 ThreadLocalMap 對象将會和 Thread 線程的 ThreadLocalMap 核心數組 Entry[] 不再有“瓜葛”。那麼,即使線上程池使用背景下,線程資源可以被反複利用于請求處理,而每次處理依賴的 ThreadLocal 對象将會被 GC 所回收,進而杜絕産生“記憶體洩漏”。

一文帶你讀懂JDK源碼:ThreadLocal類
一文帶你讀懂JDK源碼:ThreadLocal類

記憶體洩漏&規避手段 

ThreadLocal 的 remove() 源碼分析裡所提到的:

由于 Thread 單獨維護了一個 ThreadLocalMap 核心數組 Entry[],是以産生的 ThreadLocal 對象會始終與 Entry[] 數組存在一個引用鍊的關系。

由于 Entry元素 是弱引用,隻有當GC發生時才會回收掉這部分資源,假如生産環境下JVM一直沒有觸發 GC 回收,那麼會導緻許多無效過期的 Entry元素 仍舊與 目前線程 Thread 存在引用鍊的關系。

由于SpringMVC 使用線程池來處理請求的,當某個請求被處理完成之後,目前線程Thread不會立即被銷毀掉(然後會重複利用在處理其他請求);

這就導緻一個問題了:可能存在同一個線程的ThreadLocal資料被後面的請求使用(髒資料);

解決手段:我們需要在使用完ThreadLocal之後,進行一次remove()。

一文帶你讀懂JDK源碼:ThreadLocal類

replaceStaleEntry()方法講解

首先,我們定位到ThreadLocal 的 set() 方法源碼:可以看到如果出現了某個Entry元素的 key 被JVM回收了,會出現 key 為null的情況,是以需要使用 replaceStaleEntry(key, value, i);

調用了 replaceStaleEntry() 方法的時機是Entry的Key過期被GC清理了。

下面我們走讀下 ThreadLocal 的 replaceStaleEntry() 方法源碼:(主要邏輯都在注釋上了,确實要花些心思去了解,脈絡就是)

一文帶你讀懂JDK源碼:ThreadLocal類
一文帶你讀懂JDK源碼:ThreadLocal類

總結

這篇對 ThreadLocal 的源碼分析有些冗長,前前後後也花了比較多時間去構圖和思考源碼的思路,一番思考下來,也頓悟了不少知識點,包括:哈希沖突、虛引用、線程封閉等。

其實,我們看源碼目的不是去維護源碼,而是通過閱讀别人的代碼來開拓思路,這樣我們在日常寫業務時,能更加注重細節的把握,寫出高性能代碼;希望文章内容能對大家有所幫助~

掃描二維碼

擷取技術幹貨

背景技術彙

一文帶你讀懂JDK源碼:ThreadLocal類