Netty原始碼學習8——從ThreadLocal到FastThreadLocal(如何讓FastThreadLocal記憶體洩漏doge)

Cuzzz發表於2023-12-10

系列文章目錄和關於我

一丶引入

在前面的netty原始碼學習中經常看到FastThreadLocal的身影,這一篇我們將從ThreadLocal說起,來學習FastThreadLocal的設計(《ThreadLocal原始碼學習筆記》

二丶從ThreadLocal說起

ThreadLocal是JDK中實現執行緒隔離的一個工具類。實現執行緒隔離maybe你第一反應會做出Map<Thread,V>的設計,但是Map在高併發的情況下需要使用鎖or cas 來實現執行緒安全(如ConcurrentHashMap)鎖or cas都將帶來額外的開銷。

那麼ThreadLocal是如何實現的暱:

1.ThreadLocal基本結構

其基本結構如下:

image-20231210114501908
  • 每一個Thread物件都有一個名為threadLocals型別為ThreadLocal.ThreadLocalMap的屬性。
  • ThreadLocal.ThreadLocalMap物件內部存在一個Entry陣列,其中儲存的Entry物件key是ThreadLocal,value便是我們繫結線上程上的值。
  • ThreadLocal可以做到執行緒隔離是由於每一個執行緒物件持有一個ThreadLocalMap,每一個執行緒對ThreadLocalMap的處理是互不影響的。

2.ThreadLocal的優秀設計

2.1 執行緒內部屬性實現執行緒隔離,避免鎖競爭

如果使用Map<Thread,V>,不可避免的要處理執行緒安全問題,但是ThreadLocal巧妙的在Thread內部使用ThreadLocalMap來避免此問題

2.2 對開發者遮蔽細節

如果你不深入看ThreadLocal的原始碼,maybe你會認為是ThreadLocal裡面儲存了資料。你只需要使用ThreadLocal#get,set,remove即可,你完全不需要關注其底層細節。

對開發者來說好像ThreadLocal就是儲存貨物的倉庫,其實ThreadLocal只是開啟倉庫的鑰匙(使用ThreadLocal去ThreadLocalMap獲取value)

2.3 巧妙的利用弱引用避免記憶體洩漏

上面我們瞭解到ThreadLocal是ThreadLocalMap中的key,思考一下,如果使用ThreadLocal#set但是沒呼叫ThreadLocal#remove,是不是意味著ThreadLocalMap中一直會儲存這個ThreadLocal和對應的Value暱?

答案是No,ThreadLocal巧妙的使用了弱引用來解決這個問題

image-20231210120228319

ThreadLocalMap中儲存的Entry繼承了WeakRefrence,根據上面的原始碼可以看出Entry對ThreadLocal是弱引用

  • 因此:在垃圾回收器執行緒掃描它所管轄的記憶體區域的過程中,一旦發現了只具有弱引用指向的物件,不管當前記憶體空間足夠與否,都會回收它的記憶體。
  • 也就是說,如果ThreadLocal失去強引用(比如方法中區域性變數,方法結束了也就失去了強引用),只存在Entry的弱引用,在發生GC的時候將回收ThreadLocal==>從而帶導致Entry的key為null

結合下圖看一下

image-20220912172845742

細心的朋友這時候會指出:“key被回收了,value還存在哦,一樣可能存在記憶體洩漏哦”

是的,但是ThreadLocal還留了一手:即在下次呼叫其他ThreadLocal#get,set的時候,會幫助我們清理

清理什麼?清理entry陣列中key為null的entry物件

為什麼可以清理,因為此Entry中的ThreadLocal失去了強引用,不會再被使用到了

妙!

2.4 使用線性探測法,而不是拉鍊法

image-20231210121519691

上面我們說到每一個Thread中有一個ThreadLocalMap,其內部使用Entry陣列儲存多個ThreadLocal和ThreadLocal#set傳入的value

ThreadLocal#get就是從Entry陣列中拿出Entry從而獲取value

那麼怎麼根據ThreadLocal從table中快速定位到Entry暱?hash又是hash,使用hash和陣列長度取模即可!

image-20231210121848108

Hash雖好,但是不要忘記Hash衝突哦!ThreadLocal解決hash衝突使用了線性探測法,而不是拉鍊法。

下圖是拉鍊法

image-20231210122033193

下圖是線性探測法:如果找不到可以存放的位置,那麼繼續探測下去,直至擴容

image-20231210122140220

那為什麼說ThreadLocal使用線性探測法妙暱?

  • 空間效率:ThreadLocal使用陣列儲存資料,意味著資料在記憶體上是連續的,可以更好的利用CPU快取減少定址開銷。如果使用拉鍊法將Entry來需要額外的儲存下一個元素的引用指標,帶來額外的開銷
  • 時間效率:通常ThreadLocal不會儲存太多元素,線性探測法在處理衝突時更快——因為陣列儲存在記憶體上更加連續,可以更好的利用記憶體預讀能力,避免了連結串列記憶體引用導致了快取未命中。

其中時間效率這一點是建立在ThreadLocalMap中不會儲存太多元素導致hash衝突嚴重的情況下,如果元素太多ThreadLocalMap也會進行擴容

image-20231210123235866

如上:當前元素大於負載的3/4那麼進行擴容

三丶FastThreadLocal 原始碼淺析

上面說了ThreadLocal的原理和其優秀設計,那麼為什麼還需要FastThreadLocal暱?

如同FastThreadLocal的名字一樣,它在高併發的情況下擁有更高的效能!

1.FastThreadLocal最佳實踐

我們結合Netty原始碼看看netty是如何使用FastThreadLocal的

  • 使用FastThreadLocalThread

    image-20231210152637360

    netty在建立EventLoopGroup中的執行緒的時候,預設使用DefaultThreadFactory,它會建立出FastThreadLocalThread

    image-20231210152850192

    至於為什麼要是有FastThreadLocalThread,我們後面再分析

  • 將Runnable包裝為FastThreadLocalRunnable

    image-20231210160002921

    Netty會使用FastThreadLocalRunnable對原Runnable進行包裝,確保Runnable指向完後進行FastThreadLocal#removeAll釋放

    image-20231210153231219

    這一點再工作也經常使用,比如在分散式鏈路追蹤使用多執行緒處理業務邏輯,也需要將traceId對應的ThreadLocal進行傳遞和釋放,也是類似的手法。

  • 使用

    image-20231210153647335

    使用上和ThreadLocal類似

2.FastThreadLocalThread

image-20231210160104490

可以看到FastThreadLocalThread是繼承了Thread,其中內部有一個InternalThreadLocalMap型別的屬性,這便是FastThreadLocal實現的奧秘。

3.InternalThreadLocalMap

InternalThreadLocalMap 中有兩個關鍵的屬性

image-20231210160519161

  • ThreadLocal<InternalThreadLocalMap> slowThreadLocalMap,如果使用了FastThreadLocal,但是當前執行緒不是FastThread,那麼會從這個ThreadLocal中獲取InternalThreadLocalMap

  • indexedVariables,除0之外的位置儲存執行緒隔離資料,0位置儲存所有的FastThreadLocal物件

    image-20231210160719300

3.FastThreadLocal原始碼解析

3.1 get

image-20231210161621516

可以看到get就是獲取當前執行緒的InternalThreadLocalMap,然後根據index獲取內容(如果是預設值,那麼會呼叫initialize方法進行初始化)

每一個FastThreadLocal對應一個唯一的index,在FastThreadLocal構造的時候呼叫InternalThreadLocalMap#nextVariableIndex產生(使用AtomicInteger自旋+cas產生)

image-20231210161736020

image-20231210161825990

如下是InternalThreadLocalMap#get方法原始碼,可以看到根據當前執行緒是否是FastThreadLocalThread有不同的動作

image-20231210161858213

如果是FastThreadLocalThread那麼直接獲取屬性即可

image-20231210162017477

如果非FastThreadLocalThread那麼從ThreadLocal<InternalThreadLocalMap> slowThreadLocalMap中獲取

image-20231210162111950

3.2 initialize

如果FastThreadLocal中沒用值,那麼會呼叫initialValue進行初始化,initialValue是netty留給子類的擴充套件的方法

image-20231210162243701

初始化之後會設定到InternalThreadLocalMap中,並呼叫addToVariablesToRemove將當前FastThreadLocal加入到variablesToRemove中,variablesToRemove位於InternalThreadLocalMap陣列的0位置,即如下紅色框內容

image-20231210162513046

image-20231210162349394

3.3 set

image-20231210162815328

可以看到如果存入的值不是預設值,那麼呼叫setKnownNotUnset進行設定

反之呼叫remove進行刪除

3.3.1 setKnownNotUnset

image-20231210162927420

setIndexedVariable 就是向InternalThreadLocalMap中設定內容,

  • 在當前index小於陣列長度的時候會直接進行設定

    如果舊值是UNSET預設值那麼說明之前沒用設定過,進而呼叫addToVariablesToRemove將當前FastThreadLocal設定到InternalThrealLocal陣列下標為1的Set中

image-20231210162951518

  • 如果當前index大於等於陣列長度,相當於出現了hash衝突,這時候不會進行拉鍊,也不會進行線性探測,而是擴容,擴容邏輯如下

    image-20231210163429378

    首先是擴容到最接近當前index且大於index的2次冪大小(和hashMap一個道理)然後進行Arrays#copy實現陣列複製,並儲存當前值

    這裡可以看出FastThreadLocal快在哪裡,設定值的時候使用擴容來解決hash衝突,雖然導致了一些空間的浪費,但是這也使得get的時候可以根據index直接獲取資料,避免了線性探測的定址,從而有更高的效能!

3.4 remove

remove分為兩步,一是從InternalThreadLocalMap中移除index對應的元素,然後從InternalThreadLocal下標為0的Set中刪除

image-20231210164400786

3.5 removeAll

FastThreadLocalRunnable在run方法指向完後自動指向此方法,即刪除當前執行緒所有的FastThreadLocal內容,避免記憶體洩漏

image-20231210164639220

四丶總結與思考

1.FastThreadLocal快在哪裡

空間換時間,ThreadLocal慢線上性探測,那麼直接透過更大陣列空間的開闢,避免線性探測,這是一種空間換時間的思想

2. FastThreadLocal為什麼不使用弱引用

追求極致的效能,使用弱引用帶來如下缺點

  • GC開銷:弱引用需要GC垃圾收集器額外的工作來確定何時回收物件,netty這種對效能敏感的網路框架,頻繁的gc帶來不可預測的延遲

  • 訪問速度:使用弱引用可以讓Entry中key被回收,但是value還是存在,因此ThreadLocal會在get,set,等方法中檢測key為null的元素進行刪除,這也會帶來一定的開銷

  • 顯示控制:上面我們看到,FastThreadLocalThread會將runnable進行包裝保證最後進行釋放,一定程度上保證

    image-20231210170832452

3.如何讓FastThreadLocal記憶體洩漏 doge

結合FastThreadLocal的原理,我們只要我不顯示釋放,也不讓Runnable保證為FastThreadLocalRunnable,那麼就不會被釋放

image-20231210171244273

如上這個例子,會持續輸出 "洩露啦",但是如果使用ThreadLocal,再下次使用ThreadLocal的get,set方法的時候就會自動進行清理!

相關文章