每一個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
拿到當前執行緒Thread.currentThread()
這是一個Native方法,getMap
方法便是獲取執行緒中的ThreadLocal.ThreadLocalMap threadLocals
屬性,包裝成方法便於子類重寫覆蓋。如果當前執行緒的ThreadLocalMap
不為空那麼向ThreadLocalMap
中設定值,反之呼叫createMap
初始化map。通常第一次設定值的時候ThreadLocalMap
為空。
方法很簡單直接呼叫ThreadLocalMap的建構函式,在研究此建構函式之前我們先看下ThreadLocalMap的結構,其包含一個Entry
陣列,其中Entry繼承了WeakReference
我們知道弱參照具備的性質:在垃圾回收器執行緒掃描它所管轄的記憶體區域的過程中,一旦發現了只具有弱參照指向的物件,不管當前記憶體空間足夠與否,都會回收它的記憶體。這裡使用弱應用是為了防止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)的元素。
這裡使用到ThreadLocal.threadLocalHashCode
此值由nextHashCode
方法生成,其使用AtomicInteger
原子類生成
其中firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1
是為了讓hash分佈均勻減少hash衝突(類似於HashMap中高位低位進行互斥或),至於為什麼使用0x61c88647
我沒有深究。
setThreshold
方法是使用屬性threshold
記錄當前Entry陣列長度的2/3
作為擴容閾值,擴容邏輯後續進行解析。
set方法的邏輯可以分作兩部分:1.使用開放地址法找到合適的位置儲存資料,2.向陣列中放入新Entry,有需要的話擴容
第一個if 意味著是相同的ThreadLocal,類似於HashMap put相同key的元素多次,後續的後覆蓋前面的,這裡也一樣,進行覆蓋。
第二個if 意味著,原來霸佔Entry陣列位置的ThreadLocal弱應用持有的ThreadLocal被回收了會呼叫replaceStaleEntry
覆蓋
上面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進行低位不變,高位增加一個陣列長度的操作,還是使用開放地址法找到合適的位置。
這部分程式碼分為兩部分看:
獲取當前執行緒中的ThreadLocalMap屬性,以當前ThreadLocal作為key獲取到對應的值,具體獲取的邏輯在ThreadLocalMap#getEntry
方法
首先是對Entry陣列的長度進行取模,獲取當前ThreadLocal對應的位置,如果存在,且Entry中的ThreadLocal和當前入參的ThreadLocal相同(之所以需要這麼判斷是因為,hash衝突後當前ThreadLocal會被放在後續的位置,只有二者的地址相同才能返回),那麼返回。之所以判斷e!=null
可能是當前執行緒先刪除再get,這時候不判斷會丟擲空指標。
getEntryAfterMiss
方法並不複雜,就是利用nextIndex
找下一個位置,類似於HashMap中拉鍊法需要遍歷連結串列一樣,如果下一個位置為null,說明當前ThreadLocal沒有儲存過,直接返回null
這裡會直接呼叫setInitialValue
方法
其中initialValue()
方法是給子類複寫提供的方法,我們可以如下為ThreadLocal設定初始值
也可以使用ThreadLocal提供的靜態工廠方法,如
使用此靜態方法返回的是SuppliedThreadLocal
其initialValue
方法會呼叫傳入的Supplier,兩種方法都可以自定義ThreadLocal沒有設定值的時候返回的初始值
首先自然是獲取當前執行緒的ThreadLocalMap,如果初始化了才進行刪除,然後呼叫ThreadLocalMap#remove
方法,把當前ThreadLocal作為key
刪除過期條目的expungeStaleEntry
方法,會將Entry陣列中過期的條目(弱參照被回收,或者被刪除的條目)置為null。
這裡說的繼承是指父執行緒往InheritableThreadLocal
設定了值,然後父執行緒開啟子執行緒,子執行緒的InheritableThreadLocal
會拷貝其中的值
如上圖,執行test5()
方法的執行緒是main執行緒,首先向其中設定值parent
,然後開啟子執行緒,子執行緒執行直接使用get並列印出parent
。具體原理是Thread
的構造方法會拿到當前執行緒中的inheritableThreadLocals
內容複製到子執行緒的inheritableThreadLocals
中
這裡呼叫了ThreadLocal.createInheritedMap(parent.inheritableThreadLocals)
將返回值設定到建立執行緒的inheritableThreadLocals
屬性上
邏輯也很簡單,遍歷父執行緒中的entry元素,呼叫childValue
方法,實現父Entry值對映成子Entry值(InheritableThreadLocal
預設直接資訊對映,如有需要可以覆蓋childValue
方法),然後使用開放地址法存到子執行緒中。
其中InheritableThreadLocal
還重寫了getMap
,createMap
方法,二者都操作Thread中的inheritableThreadLocals
屬性