1. 程式人生 > >ThreadLocal父子線程之間的數據傳遞問題

ThreadLocal父子線程之間的數據傳遞問題

tab smi github 通過 hashmap 處理 直接 復用 java

一、問題的提出

在系統開發過程中常使用ThreadLocal進行傳遞日誌的RequestId,由此來獲取整條請求鏈路。然而當線程中開啟了其他的線程,此時ThreadLocal裏面的數據將會出現無法獲取/讀取錯亂,甚至還可能會存在內存泄漏等問題,下面用代碼來演示一下這個問題。

普通代碼示例:

技術分享圖片

並行流代碼示例:

技術分享圖片

二、問題的解決

ThreadLocal的子類InheritableThreadLocal其實已經幫我們處理好了,通過這個組件可以實現父子線程之間的數據傳遞,在子線程中能夠父線程中的ThreadLocal本地變量。

三、源碼的分析

技術分享圖片

可以看出InheritableThreadLocal繼承自ThreadLocal,並重寫了三個相關方法。

再回來過來看ThreadLocal的源碼:

技術分享圖片

我們發現InheritableThreadLocal中createMap,以及getMap方法處理的對象不一樣了,其中在ThreadLocal中處理的是threadLocals,而InheritableThreadLocal中的是inheritableThreadLocals,我們再順藤摸瓜看一下Thread對象的處理,其中在init源碼中我們看到這麽一段代碼:

技術分享圖片

代碼的意思是在Thread獲取先父親線程parent(即要創建子線程的當前這個線程)。當父親線程中對inherThreadLocals進行了賦值,就會把當前線程的本地變量(也就是父線程的inherThreadLocals)進行createInheritedMap方法操作。查看源碼createInheritedMap方法,源碼可知此操作就是將賦線程的threadLocalMap傳遞給子線程。

技術分享圖片

我們寫個代碼測試一下:

技術分享圖片

看起來似乎真的是解決了我們無法傳遞的問題。

四、真的就這麽美好麽?我們來和線程池搭配一下

技術分享圖片

測試結果顯示兩次賦值,得到的結果還是第一次的值!為什麽?

其實原因也很簡單,我們的線程池會緩存使用過的線程。當線程需要被重復利用的時候,並不會再重新執行init()初始化方法,而是直接使用已經創建過的線程,所以這裏的值不會二次產生變化,那麽該怎麽做到真正的父子線程數據傳遞呢?

五、真正的解決方案:阿裏的transmittable-thread-local了解一下

JDK的InheritableThreadLocal類可以完成父線程到子線程的值傳遞。但對於使用線程池等會池化復用線程的組件的情況,線程由線程池創建好,並且線程是池化起來反復使用的;這時父子線程關系的ThreadLocal值傳遞已經沒有意義,應用需要的實際上是把任務提交給線程池時的ThreadLocal值傳遞到任務執行時。

首先分析一下最核心的類:TransmittableThreadLocal

技術分享圖片

首先TransmittableThreadLocal繼承自InheritableThreadLocal,這樣可以在不破壞原有InheritableThreadLocal特性的情況下,還能充分使用Thread線程創建過程中執行init方法,從而達到父子線程傳遞數據的目的。

這裏有一個很重要的變量holder:源碼如下

1. holder中存放的是InheritableThreadLocal本地變量。

2. WeakHashMap支持存放空置。

技術分享圖片

主要的幾個相關方法:源碼如下

1. get方法調用時,先獲取父親的相關數據判斷是否有數據,然後在holder中把自身也給加進去。

2. set方法調用時,先在父親中設置,再本地判斷是holder否為刪除或者是新增數據。

3. remove調用時,先刪除自身,再刪除父親中的數據,刪除也是直接以自身this作為變量Key。

技術分享圖片

采用包裝的形式來處理線程池中的線程不會執行初始化的問題,源碼如下:

1. 先取得holder。

2. 備份線程本地數據

3. run原先的方法

4. 還原線程本地數據

技術分享圖片

備份方法:

1. 先獲取holder中的數據

2. 進行叠代,數據在captured中不存在,但是holder中存在,說明是後來加進去的,進行刪除。

3. 再將captured設置到當前線程中。

技術分享圖片

還原方法:

1. 先獲取holder中的數據

2. backup中不存在,holder中存在,說明是後面加進去的,進行刪除還原操作。

3. 再將backup設置到當前線程中。

技術分享圖片

下面是幾個典型場景例子。

1. 分布式跟蹤系統

2. 日誌收集記錄系統上下文

3. 應用容器或上層框架跨應用代碼給下層SDK傳遞信息

項目地址:https://github.com/alibaba/transmittable-thread-local

ThreadLocal父子線程之間的數據傳遞問題