ThreadLocal源碼學習筆記

系列文章目錄和關於我

一丶ThreadLocal結構

image-20220912155015204

每一個Thread對象都有一個名為threadLocals類型為ThreadLocal.ThreadLocalMap的屬性,ThreadLocal.ThreadLocalMap對象內部存在一個Entry數組,其中存儲的Entry對象key是ThreadLocal,value便是我們綁定在執行緒上的值。ThreadLocal可以做到執行緒隔離是由於每一個執行緒對象持有一個ThreadLocalMap,每一個執行緒對ThreadLocalMap的處理是互不影響的。之所以持有的是ThreadLocalMap,是執行緒可能使用多個ThreadLocal存儲數據,比如在Spring事務同步管理器中TransactionSynchronizationManager包含三個ThreadLocal對象,一個管理事務相關資源,一個管理當前事務需要回調的同步介面,一個管理事務名稱,三個ThreadLocal對象對應著當前Thread持有的ThreadLocal.ThreadLocalMap中Entry數組的的三個Entry

二丶源碼學習

1.set(T value)——向ThreadLocal中設置值

image-20220912162732491

拿到當前執行緒Thread.currentThread()這是一個Native方法,getMap方法便是獲取執行緒中的ThreadLocal.ThreadLocalMap threadLocals屬性,包裝成方法便於子類重寫覆蓋。如果當前執行緒的ThreadLocalMap 不為空那麼向ThreadLocalMap 中設置值,反之調用createMap初始化map。通常第一次設置值的時候ThreadLocalMap為空。

2.createMap(Thread t, T firstValue)——為執行緒初始化ThreadLocalMap

image-20220912163337105

方法很簡單直接調用ThreadLocalMap的構造函數,在研究此構造函數之前我們先看下ThreadLocalMap的結構,其包含一個Entry數組,其中Entry繼承了WeakReference

image-20220912164142408

2.1為什麼這裡Entry保存ThreadLocal類型的key使用弱引用:

我們知道弱引用具備的性質:在垃圾回收器執行緒掃描它所管轄的記憶體區域的過程中,一旦發現了只具有弱引用指向的對象,不管當前記憶體空間足夠與否,都會回收它的記憶體。這裡使用弱應用是為了防止oom,如果ThreadLocal作為Key不使用弱引用,且根據可達性演算法此ThreadLocal以及無法和GCRoot關聯,但是當前執行緒並沒有結束,可以通過當前執行緒關聯到其ThreadLocalMap,再關聯到Entry中的ThreadLocal對象,這時候ThreadLocal將永遠無法被回收。

image-20220912171424317

這裡我們給出一個啟動執行緒執行死循環,再死循環中創建ThreadLocal並set,這段程式碼執行並不會發生OOM,原因是ThreadLocal是被弱引用指向,再發生GC的時候會被回收。

image-20220912172845742

這裡應該還有一個問題,雖然ThreadLocal被回收了,但是Entry數組一致再塞入Entry,回收之後就相當於Entry的key為null,value存在值,那麼為什麼不會oom昵,原因是往ThreadLocalMap中塞入元素的時候,會刪除掉過時(指Entry中的key弱引用持有的ThreadLocal為null)的元素。

2.2 ThreadLocalMap構造方法

image-20220912175632569

這裡使用到ThreadLocal.threadLocalHashCode此值由nextHashCode方法生成,其使用AtomicInteger原子類生成

image-20220912175855697

其中firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1是為了讓hash分布均勻減少hash衝突(類似於HashMap中高位地位進行異或),至於為什麼使用0x61c88647我沒有深究。

setThreshold方法是使用屬性threshold記錄當前Entry數組長度的2/3作為擴容閾值,擴容邏輯後續進行解析。

3.ThreadLocalMap#set(ThreadLocal<?> key, Object value) 存入數據

set方法的邏輯可以分作兩部分:1.使用開放地址法找到合適的位置存儲數據,2.向數組中放入新Entry,有需要的話擴容

3.1.使用開放地址法找到合適的位置存儲數據

image-20220912182943738

第一個if 意味著是相同的ThreadLocal,類似於HashMap put相同key的元素多次,後續的後覆蓋前面的,這裡也一樣,進行覆蓋。

第二個if 意味著,原來霸佔Entry數組位置的ThreadLocal弱應用持有的ThreadLocal被回收了會調用replaceStaleEntry覆蓋

3.2向數組中放入新Entry,有需要的話擴容

上面for循環進行的條件是e != null,e是Entry數組中元素,那麼結束for循環,除了成功覆蓋原有元素的還有找到一個可以使用的位置

image-20220912183722977

這裡擴容的條件有兩個cleanSomeSlots刪除過期的條目失敗,且 當前Entry數組存入元素大於擴容閾值

image-20220912184341958

擴容程式碼如下,遍歷所有的元素,如果已經被回收了那麼將value也置為null,如果沒有被回收那麼將元素拷貝到新的位置

image-20220912185926597

這裡為什麼要將value也置為空昵

首先ThreadLocal的key 已經被回收了,這時候調用者沒辦法拿到被回收key對應的value,所有置為null是不會影響到使用的。

關鍵的是Help the GC的注釋,置為null可以幫助jvm進行GC,我們首先看下如下方法

image-20220912190508878

此方法也不會發生OOM

image-20220912190751128

理論直接將被回收Entry位置的元素置為null,這時候也是無法通過GC Root應用到Entry,自然也無法引用到String對象,直接置為null也是相應的目的

這裡擴容複製元素沒有像HashMap進行地位不變,高位翻倍的操作,還是使用開放地址法找到合適的位置。

4.ThreadLocal#get()——獲取和當前執行緒綁定在此ThreadLocal上的值

image-20220912191809282

這部分程式碼分為兩部分看:

4.1獲取ThreadLocalMap中的值

獲取當前執行緒中的ThreadLocalMap屬性,以當前ThreadLocal作為key獲取到對應的值,具體獲取的邏輯在ThreadLocalMap#getEntry方法

image-20220912192702025

首先是對Entry數組的長度進行取模,獲取當前ThreadLocal對應的位置,如果存在,且Entry中的ThreadLocal和當前入參的ThreadLocal相同(之所以需要這麼判斷是因為,hash衝突後當前ThreadLocal會被放在後續的位置,只有二者的地址相同才能返回),那麼返回。之所以判斷e!=null可能是當前執行緒先刪除再get,這時候不判斷會拋出空指針。

image-20220912193722607

getEntryAfterMiss方法並不複雜,就是利用nextIndex找下一個位置,類似於HashMap中拉鏈法需要遍歷鏈表一樣,如果下一個位置為null,說明當前ThreadLocal沒有存儲過,直接返回null

4.2ThreadLocalMap沒有初始化,或者沒有從ThreadLocalMap中獲取到對應的值

這裡會直接調用setInitialValue方法

image-20220912194221872

其中initialValue()方法是給子類複寫提供的方法,我們可以如下為ThreadLocal設置初始值

image-20220912194358814

也可以使用ThreadLocal提供的靜態工廠方法,如

image-20220912194441817

使用此靜態方法返回的是SuppliedThreadLocal其initialValue方法會調用傳入的Supplie,兩種方法都可以自定義ThreadLocal沒有設置值的時候返回的初始值

5.ThreadLocal#remove()

image-20220912194843907

首先自然是獲取當前執行緒的ThreadLocalMap,如果初始化了才進行刪除,然後調用ThreadLocalMap#remove方法,把當前ThreadLocal作為key

image-20220912195055884

刪除過期條目的expungeStaleEntry方法,會將Entry數組中過期的條目(弱引用被回收,或者被刪除的條目)置為null。

三丶InheritableThreadLocal支援繼承的ThreadLocal

這裡說的繼承是指父執行緒往InheritableThreadLocal設置了值,然後父執行緒開啟子執行緒,子執行緒的InheritableThreadLocal會拷貝其中的值

image-20220912205839246

如上圖,運行test5()方法的執行緒是main執行緒,首先向其中設置值parent,然後開啟子執行緒,子執行緒運行直接使用get並列印出parent。具體原理是Thread的構造方法會拿到當前執行緒中的inheritableThreadLocals內容複製到子執行緒的inheritableThreadLocals中

image-20220912213255396

這裡調用了ThreadLocal.createInheritedMap(parent.inheritableThreadLocals)將返回值設置到創建執行緒的inheritableThreadLocals屬性上

image-20220912214050852

邏輯也很簡單,遍歷父執行緒中的entry元素,調用childValue方法,實現父Entry值映射成子Entry值(InheritableThreadLocal默認直接資訊映射,如有需要可以覆蓋childValue方法),然後使用開放地址法存到子執行緒中。

其中InheritableThreadLocal還重寫了getMap,createMap方法,二者都操作Thread中的inheritableThreadLocals屬性

image-20220912221812268

Tags: