一诺祸、問題的提出
在系統(tǒng)開發(fā)過程中常使用ThreadLocal進(jìn)行傳遞日志的RequestId携悯,由此來獲取整條請求鏈路。然而當(dāng)線程中開啟了其他的線程筷笨,此時(shí)ThreadLocal里面的數(shù)據(jù)將會(huì)出現(xiàn)無法獲茹竟怼/讀取錯(cuò)亂,甚至還可能會(huì)存在內(nèi)存泄漏等問題胃夏,下面用代碼來演示一下這個(gè)問題轴或。
普通代碼示例:
并行流代碼示例:
[圖片上傳中...(image-b7fe28-1545751320761-12)]
二、問題的解決
ThreadLocal的子類InheritableThreadLocal其實(shí)已經(jīng)幫我們處理好了仰禀,通過這個(gè)組件可以實(shí)現(xiàn)父子線程之間的數(shù)據(jù)傳遞照雁,在子線程中能夠父線程中的ThreadLocal本地變量。
三答恶、源碼的分析
可以看出InheritableThreadLocal繼承自ThreadLocal饺蚊,并重寫了三個(gè)相關(guān)方法。
再回來過來看ThreadLocal的源碼:
我們發(fā)現(xiàn)InheritableThreadLocal中createMap悬嗓,以及getMap方法處理的對象不一樣了污呼,其中在ThreadLocal中處理的是threadLocals,而InheritableThreadLocal中的是inheritableThreadLocals包竹,我們再順藤摸瓜看一下Thread對象的處理燕酷,其中在init源碼中我們看到這么一段代碼:
代碼的意思是在Thread獲取先父親線程parent(即要?jiǎng)?chuàng)建子線程的當(dāng)前這個(gè)線程)。當(dāng)父親線程中對inherThreadLocals進(jìn)行了賦值映企,就會(huì)把當(dāng)前線程的本地變量(也就是父線程的inherThreadLocals)進(jìn)行createInheritedMap方法操作悟狱。查看源碼createInheritedMap方法静浴,源碼可知此操作就是將賦線程的threadLocalMap傳遞給子線程堰氓。
我們寫個(gè)代碼測試一下:
看起來似乎真的是解決了我們無法傳遞的問題。
四苹享、真的就這么美好么双絮?我們來和線程池搭配一下
測試結(jié)果顯示兩次賦值,得到的結(jié)果還是第一次的值得问!為什么囤攀?
其實(shí)原因也很簡單,我們的線程池會(huì)緩存使用過的線程宫纬。當(dāng)線程需要被重復(fù)利用的時(shí)候焚挠,并不會(huì)再重新執(zhí)行init()初始化方法,而是直接使用已經(jīng)創(chuàng)建過的線程漓骚,所以這里的值不會(huì)二次產(chǎn)生變化蝌衔,那么該怎么做到真正的父子線程數(shù)據(jù)傳遞呢榛泛?
五、真正的解決方案:阿里的transmittable-thread-local了解一下
JDK的InheritableThreadLocal類可以完成父線程到子線程的值傳遞噩斟。但對于使用線程池等會(huì)池化復(fù)用線程的組件的情況曹锨,線程由線程池創(chuàng)建好,并且線程是池化起來反復(fù)使用的剃允;這時(shí)父子線程關(guān)系的ThreadLocal值傳遞已經(jīng)沒有意義沛简,應(yīng)用需要的實(shí)際上是把任務(wù)提交給線程池時(shí)的ThreadLocal值傳遞到任務(wù)執(zhí)行時(shí)。
首先分析一下最核心的類:TransmittableThreadLocal
首先TransmittableThreadLocal繼承自InheritableThreadLocal斥废,這樣可以在不破壞原有InheritableThreadLocal特性的情況下椒楣,還能充分使用Thread線程創(chuàng)建過程中執(zhí)行init方法,從而達(dá)到父子線程傳遞數(shù)據(jù)的目的牡肉。
這里有一個(gè)很重要的變量holder:源碼如下
1. holder中存放的是InheritableThreadLocal本地變量撒顿。
2. WeakHashMap支持存放空置。
主要的幾個(gè)相關(guān)方法:源碼如下
1. get方法調(diào)用時(shí)荚板,先獲取父親的相關(guān)數(shù)據(jù)判斷是否有數(shù)據(jù)凤壁,然后在holder中把自身也給加進(jìn)去。
2. set方法調(diào)用時(shí)跪另,先在父親中設(shè)置拧抖,再本地判斷是holder否為刪除或者是新增數(shù)據(jù)。
3. remove調(diào)用時(shí)免绿,先刪除自身唧席,再刪除父親中的數(shù)據(jù),刪除也是直接以自身this作為變量Key嘲驾。
采用包裝的形式來處理線程池中的線程不會(huì)執(zhí)行初始化的問題淌哟,源碼如下:
1. 先取得holder。
2. 備份線程本地?cái)?shù)據(jù)
3. run原先的方法
4. 還原線程本地?cái)?shù)據(jù)
備份方法:
1. 先獲取holder中的數(shù)據(jù)
2. 進(jìn)行迭代辽故,數(shù)據(jù)在captured中不存在徒仓,但是holder中存在,說明是后來加進(jìn)去的誊垢,進(jìn)行刪除掉弛。
3. 再將captured設(shè)置到當(dāng)前線程中。
還原方法:
1. 先獲取holder中的數(shù)據(jù)
2. backup中不存在喂走,holder中存在殃饿,說明是后面加進(jìn)去的,進(jìn)行刪除還原操作芋肠。
3. 再將backup設(shè)置到當(dāng)前線程中乎芳。
下面是幾個(gè)典型場景例子。
1. 分布式跟蹤系統(tǒng)
2. 日志收集記錄系統(tǒng)上下文
3. 應(yīng)用容器或上層框架跨應(yīng)用代碼給下層SDK傳遞信息
項(xiàng)目地址:https://github.com/alibaba/transmittable-thread-local