ThreadLocal原始碼學習筆記

2022-09-13 06:18:45

系列文章目錄和關於我

一丶ThreadLocal結構

每一個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中設定值

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

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

方法很簡單直接呼叫ThreadLocalMap的建構函式,在研究此建構函式之前我們先看下ThreadLocalMap的結構,其包含一個Entry陣列,其中Entry繼承了WeakReference

2.1為什麼這裡Entry儲存ThreadLocal型別的key使用弱參照:

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

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

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

2.2 ThreadLocalMap構造方法

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

其中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.使用開放地址法找到合適的位置儲存資料

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

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

3.2向陣列中放入新Entry,有需要的話擴容

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

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

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

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

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

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

此方法也不會發生OOM

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

這裡擴容複製元素沒有像HashMap進行低位不變,高位增加一個陣列長度的操作,還是使用開放地址法找到合適的位置。

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

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

4.1獲取ThreadLocalMap中的值

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

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

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

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

這裡會直接呼叫setInitialValue方法

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

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

使用此靜態方法返回的是SuppliedThreadLocalinitialValue方法會呼叫傳入的Supplier,兩種方法都可以自定義ThreadLocal沒有設定值的時候返回的初始值

5.ThreadLocal#remove()

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

刪除過期條目的expungeStaleEntry方法,會將Entry陣列中過期的條目(弱參照被回收,或者被刪除的條目)置為null。

三丶InheritableThreadLocal支援繼承的ThreadLocal

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

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

這裡呼叫了ThreadLocal.createInheritedMap(parent.inheritableThreadLocals)將返回值設定到建立執行緒的inheritableThreadLocals屬性上

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

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