ThreadLocal源碼解讀:初識ThreadLocal
從類的javadoc出發(fā)
想要深度了解一個類可以從 javadoc 出發(fā),這里可藏著不少好東西,下面讓我來帶大家盤一盤 ThreadLocal 的 javadoc!
圖片
從區(qū)域①可以看出 ThreadLocal 的用途:提供了線程緯度的局部變量。通俗來講就是每一個線程操作自己的局部變量,線程之間互不干擾。
通過這段描述我們還可以發(fā)現(xiàn)官方是建議我們將 ThreadLocal 用作類中私有的靜態(tài)成員變量。
區(qū)域②是官方為我們提供了一個小 demo,模擬了為每個線程生成線程 id 的場景,并且這個 id 在第一次調(diào)用 ThreadId.get 時被分配,并在后續(xù)調(diào)用中保持不變。
可以看到這個 demo 中將 ThreadLocal 對象使用 private static final 修飾,這也正是官方所建議的。
區(qū)域③官方著重強(qiáng)調(diào)了局部變量與線程的關(guān)系,一旦線程銷毀,局部變量也會被垃圾回收器回收掉。
常用API分析
局部變量是如何存儲的?為何又會隨著線程的銷毀而銷毀?在這個過程中 ThreadLocal 又充當(dāng)著怎樣的角色?源碼之下沒有秘密!
圖片
通過 idea 側(cè)邊欄提供的 Structure 模塊可以看出 ThreadLocal 類中的方法并不多,在使用中可以用到的也就圈出來的這幾個。
我們將這幾個方法玩明白,上面的問題也就迎刃而解了。
initialValue方法
首先我們看一下 initialValue 方法。
圖片
通過源碼我們可以看出這個方法的訪問修飾符被設(shè)置為了 protected 類型的,意味著這個方法只能被同包及其子類訪問,并且這個方法的實現(xiàn)是直接返回了 null,可以推斷出這個方法應(yīng)該是用作模板方法(鉤子函數(shù)),并且結(jié)合方法名可以判斷出這個方法作用是初始化值。
而這一切在方法的 javadoc 都有所描述,所以在看源碼的時候一定不可以忽略 javadoc,這里含有大量有用信息,即使使用翻譯軟件也一定要含淚看完。
在實踐中想要利用這個方法,必須對 ThreadLocal 進(jìn)行子類化,并重寫此方法。通常有兩種方式,其中一種是開篇的 javadoc 中使用的匿名內(nèi)部類寫法。
圖片
而匿名內(nèi)部類寫法,可以用 ThreadLocal 提供的 withInitial 方法進(jìn)行等效替換,個人更傾向于使用 withInitial 配合 lambda 表達(dá)式的寫法,可以使得代碼更加簡潔清晰。
private static final ThreadLocal<Integer> threadId = ThreadLocal.withInitial(() -> nextId. getAndIncrement());
private static final ThreadLocal<Integer> threadId = ThreadLocal.withInitial(nextId::getAndIncrement);
那么 initialValue 方法將在什么時候調(diào)用呢?先給出結(jié)論:在當(dāng)前線程對象中以 ThreadLocal 對象為 key 的局部變量值不存在時,調(diào)用 get 請求將會觸發(fā)初始化邏輯。
get方法
下面我們來看一下 get 方法,通過分析 get 方法我們就可以知道局部變量是如何存儲的。
圖片
從源碼中可以看到首先是獲取當(dāng)前的線程對象,然后通過 getMap 方法傳入當(dāng)前 Thread 對象獲取到了一個 ThreadLocalMap 對象 map。
并判斷當(dāng)前 map 是否為 null,如果不為 null 則調(diào)用 map 的 getEntry 方法,并且傳入了 this 對象,在此刻 this 對象就是 ThreadLocal 對象。
getEntry 方法返回了 Entry 對象 e,如果 e 不為 null 則返回 e 的 value 成員變量,并將其轉(zhuǎn)換為我們定義好的泛型 T 進(jìn)行返回。這里的 e.value 就是我們所說的局部變量。
圖片
我們來看一下 Entry 類的定義,通過源碼可以看出 Entry 類繼承了 WeakReference,并將引用字段作為鍵(始終是 ThreadLocal 對象),value 則定義為 Object 類型。關(guān)于弱引用問題將在下一期分析內(nèi)存泄露問題時進(jìn)行展開討論。
如果 map 為 null 或者對象 e 為 null,都將調(diào)用 setInitialValue 方法進(jìn)行初始化,而 setInitialValue 方法將會調(diào)用上述我們重寫的 initialValue 方法獲取局部變量的值,進(jìn)行初始化操作。
圖片
此時我們已經(jīng)可以確定局部變量存儲在 ThreadLocalMap 對象中。那么 ThreadLocalMap 對象又從何獲取的呢,通過點進(jìn) getMap 方法源碼(如下圖)我們可以發(fā)現(xiàn),ThreadLocalMap 是 Thread 類的成員變量,也就是說存儲在 Thread 對象中。
圖片
點進(jìn) Thread 源碼(如下圖)我們就可以看到類型為 ThreadLocalMap 的成員變量 threadLocals,并且它的初始值是 null。
圖片
那么在這個過程中 ThreadLocal 對象充當(dāng)了什么樣的角色呢?其實 ThreadLocal 對象的作用就相當(dāng)于 HashMap 中 key 的作用。我們點進(jìn)以 ThreadLocal 對象作為參數(shù)的 getEntry 方法,進(jìn)行進(jìn)一步的分析。
圖片
通過 Entry e = table[i]我們發(fā)現(xiàn) Entry 對象取自 table 這個數(shù)組,而數(shù)組 table 是 ThreadLocalMap 的類的成員變量。
圖片
數(shù)組坐標(biāo) i 通過表達(dá)式key.threadLocalHashCode & (table.length - 1)計算而來,這個表達(dá)式在 table.length 是 2 的冪次方時等同于key.threadLocalHashCode % table.length操作,具體論證的過程大家可以百度一下。
如果通過坐標(biāo) i 獲取的 Entry 對象的 key 和當(dāng)前的 ThreadLocal 對象相等,則證明當(dāng)前 Entry 對象確實是存儲了當(dāng)前線程當(dāng)前 ThreadLocal 對象的局部變量。
如果不等,說明發(fā)生了哈希沖突,則調(diào)用 getEntryAfterMiss 方法繼續(xù)搜索,直到搜索到當(dāng)前 ThreadLocal 對象對應(yīng)的 Entry,或者未搜索到返回 null。
圖片
在 getEntryAfterMiss 方法中,我們可以看到搜索的過程中調(diào)用了 nextIndex 方法進(jìn)行獲取下一次搜索的索引,nextIndex 方法的邏輯很簡單就是對 i 進(jìn)行遞增,如果等于了容量值 len 則從 0 繼續(xù)遍歷。
圖片
通過搜索邏輯我們可以推斷出 ThreadLocalMap 解決哈希沖突采用的是線性探測法,而 HashMap 在碰到哈希沖突時采用的是拉鏈法,這一點要區(qū)別記憶。
此時我們已經(jīng)可以抽離出一條引用鏈 Thread->ThreadLocalMap->Entry[]->Entry->value(局部變量),在這條引用鏈上都是強(qiáng)引用。
我們再來分析一下為何局部變量為什么會隨著線程的銷毀而銷毀呢?
JVM 垃圾回收機(jī)制默認(rèn)采取的是可達(dá)性分析算法。在這條強(qiáng)引用鏈中除了 value 調(diào)用鏈中的其他引用都是當(dāng)前對象的唯一引用。
一旦這條引用鏈的根 Thread 對象被回收,那么其他對象都將不可達(dá),都將被垃圾回收器所回收。
而局部變量如果沒有被其他對象所引用也將不可達(dá),從而被銷毀。
set方法
下面我們來看一下 set 方法的源碼,比較有意思的是,竟然和 setInitialValue 方法中的一段邏輯一毛一樣,不知道編寫時為什么沒有在 setInitialValue 方法中直接調(diào)用 set 方法。
圖片
set 方法在執(zhí)行時根據(jù) ThreadLocalMap 對象是否為 null,分別進(jìn)行賦值及初始化兩種不同處理邏輯。
這里我們先看一下 ThreadLocalMap 對象為 null 時的 createMap 方法的邏輯,這樣更有助我們理解。
圖片
在 createMap 方法內(nèi)部調(diào)用了 ThreadLocalMap 兩個參數(shù)的構(gòu)造函數(shù),并將返回的對象賦值給了當(dāng)前線程對象的成員變量 threadLocals。
我們查看 ThreadLocalMap 的構(gòu)造方法可以發(fā)現(xiàn)很多關(guān)鍵信息。
圖片
其中 ThreadLocalMap 的初始容量被設(shè)置為了 16。
圖片
并且在構(gòu)造方法的最后調(diào)用了 setThreshold 方法,該方法用于設(shè)置擴(kuò)容的閾值,這個閾值為當(dāng)前容量的 2 / 3。
圖片
當(dāng) ThreadLocalMap 對象不為 null 時將會調(diào)用 ThreadLocalMap 的 set 方法。
圖片
從方法中我們可以看出,首先依舊是根據(jù) ThreadLocal 對象計算出索引 i,然后根據(jù)當(dāng)前索引值獲取 Entry 對象。
如果 Entry 對象不為 null 則會進(jìn)行下列幾種判斷,如果 key 是當(dāng)前 ThreadLocal 對象,則將舊值替換掉。
如果當(dāng)前 key 為 null,則說明當(dāng)前 Entry 對象已經(jīng)過時,則調(diào)用替換過時 Entry 的 replaceStaleEntry 方法,這個方法將在下一期進(jìn)行展開討論。
如果 key 不為 null 且不等于當(dāng)前 ThreadLocal 對象則進(jìn)行下一輪遍歷。
如果上述邏輯未能找到當(dāng)前 ThreadLocal 對象對應(yīng)的 Entry 對象,且在這個過程中沒有過時的 Entry 對象供替換,則生成一個新的 Entry 對象放置在當(dāng)前索引 i 位置(經(jīng)過上述遍歷索引 i 已經(jīng)定位在了一個 Entry 對象為 null 的位置)。
最后再根據(jù)!cleanSomeSlots(i, sz) && sz >= threshold判斷一下是否需要進(jìn)行擴(kuò)容操作。
其中 cleanSomeSlots 方法的作用是向下執(zhí)行有限次數(shù)的掃描,看看有沒有過時的 Entry 對象可供清理,如果清理了任何個數(shù) Entry 對象將返回 true,則此時一定不需要擴(kuò)容,如果沒有清理任何 Entry 對象則需要判斷一下當(dāng)前 ThreadLocalMap 的大小是否達(dá)到了擴(kuò)容閾值。
remove方法
最后我們來看一下 remove 方法,remove 方法的作用是刪除當(dāng)前線程以當(dāng)前 ThreadLocal 對象為 key 的局部變量值。
圖片
通過源碼我們可以看出 ThreadLocal 的 remove 方法的核心邏輯是調(diào)用了 ThreadLocalMap 的 remove 方法。
圖片
ThreadLocalMap 的 remove 方法的邏輯也很清晰,根據(jù) ThreadLocal 對象搜索對應(yīng)的 Entry 對象,如果搜索到則將 Entry 對象通過 Reference 的 clear 方法設(shè)置為過時,最后調(diào)用 expungeStaleEntry 方法將過時的 Entry 條目進(jìn)行清理,expungeStaleEntry 也將在下一期進(jìn)行展開討論。
總結(jié)
最后我們再來簡單總結(jié)一下,首先每一個線程對象中都存儲了一個 ThreadLocalMap 對象,ThreadLocalMap 對象以 ThreadLocal 對象作為 key 存儲值,這個值就是我們所說的局部變量。
但是在設(shè)計的過程中并沒有直接暴露給我們操作 ThreadLocalMap 的 API,所以在這個過程中我們需要 ThreadLocal 對象作為橋梁,ThreadLocal 類包含 initialValue、get、set、remove 方法。
其中 initialValue 方法用于提供初始化 ThreadLocalMap 對象中以當(dāng)前 ThreadLocal 對象為 key 的局部變量的值。
get 方法用于獲取當(dāng)前線程以當(dāng)前 ThreadLocal 對象為 key 的局部變量,如果當(dāng)前局部變量的未初始化,則使用 initialValue 返回的值作為局部變量的值進(jìn)行初始化操作。
set 方法用于為當(dāng)前線程以當(dāng)前 ThreadLocal 對象為 key 的局部變量設(shè)置值,新值將會覆蓋舊值。
remove 方法用于刪除當(dāng)前線程以當(dāng)前 ThreadLocal 對象為 key 的局部變量值。