有小伙伴讓我再說(shuō)說(shuō)TransmittableThreadLocal(下邊統(tǒng)一簡(jiǎn)稱:TTL),它是阿里開(kāi)源的一個(gè)工具類,解決異步執(zhí)行時(shí)上下文傳遞的問(wèn)題。
那今天就來(lái)介紹介紹 TTL,補(bǔ)充下 ThreadLocal 家族的短板吧。
這篇過(guò)后,ThreadLocal 就真的一網(wǎng)打盡了!
不過(guò)還是建議先看看前置篇(文末會(huì)放鏈接),不然理解起來(lái)可能有點(diǎn)困難。
緣由
任何一個(gè)組件的出現(xiàn)必有其緣由,知其緣由背景才能更深刻地理解它。
我們知道 ThreadLocal 的出現(xiàn)就是為了本地化線程資源,防止不必要的多線程之間的競(jìng)爭(zhēng)。
在有些場(chǎng)景,當(dāng)父線程 new 一個(gè)子線程的時(shí)候,希望把它的 ThreadLocal 繼承給子線程。
這時(shí)候 InheritableThreadLocal 就來(lái)了,它就是為了父子線程傳遞本地化資源而提出的。
具體的實(shí)現(xiàn)是在子線程對(duì)象被 new 的時(shí)候,即 Thread.init 的時(shí),如果查看到父線程內(nèi)部有 InheritableThreadLocal 的數(shù)據(jù)。
那就在子 Thread 初始化的時(shí),把父線程的 InheritableThreadLocal 拷貝給子線程。
就這樣簡(jiǎn)單地把父線程的 ThreadLocal 數(shù)據(jù)傳遞給子線程了。
但是,這個(gè)場(chǎng)景只能發(fā)生在 new Thread 的時(shí)候!也就是手動(dòng)創(chuàng)建線程之時(shí)!那就有個(gè)問(wèn)題了,在平時(shí)我們使用的時(shí)候基本用的都是線程池。
那就麻了啊,線程池里面的線程都預(yù)創(chuàng)建好了,調(diào)用的時(shí)候就沒(méi)法直接用 InheritableThreadLocal 了。
所以就產(chǎn)生了一個(gè)需求,如何往線程池內(nèi)的線程傳遞 ThreadLocal?,JDK 的類庫(kù)沒(méi)這個(gè)功能,所以怎么搞?
只能我們自己造輪子了。
基于 Spring Boot + MyBatis Plus + Vue & Element 實(shí)現(xiàn)的后臺(tái)管理系統(tǒng) + 用戶小程序,支持 RBAC 動(dòng)態(tài)權(quán)限、多租戶、數(shù)據(jù)權(quán)限、工作流、三方登錄、支付、短信、商城等功能
- 項(xiàng)目地址:https://github.com/YunaiV/ruoyi-vue-pro
- 視頻教程:https://doc.iocoder.cn/video/
如何設(shè)計(jì)?
需求已經(jīng)明確了,但是怎么實(shí)現(xiàn)呢?
平時(shí)我們用線程池的話,比如你要提交任務(wù),則使用代碼如下:
Runnabletask=newRunnable....;
executorService.submit(task);
小貼士:以下的 ThreadLocal 泛指線程本地?cái)?shù)據(jù),不是指 ThreadLocal 這個(gè)類
這時(shí)候,我們想著把當(dāng)前線程的 ThreadLocal 傳遞給線程池內(nèi)部將要執(zhí)行這個(gè) task 的線程。
但此時(shí)我們哪知道線程池里面的哪個(gè)線程會(huì)來(lái)執(zhí)行這個(gè)任務(wù)?
所以,我們得先把當(dāng)前線程的 ThreadLocal 保存到這個(gè) task 中。
然后當(dāng)線程池里的某個(gè)線程,比如線程 A 獲取這個(gè)任務(wù)要執(zhí)行的時(shí)候,看看 task 里面是否有存儲(chǔ)著的 ThreadLocal 。
如果存著那就把這個(gè) ThreadLocal 放到線程 A 的本地變量里,這樣就完成了傳遞。
然后還有一步,也挺關(guān)鍵的,就是恢復(fù)線程池內(nèi)部執(zhí)行線程的上下文,也就是該任務(wù)執(zhí)行完畢之后,把任務(wù)帶來(lái)的本地?cái)?shù)據(jù)給刪了,把線程以前的本地?cái)?shù)據(jù)復(fù)原。
設(shè)計(jì)思路應(yīng)該已經(jīng)很明確了吧?來(lái)看看具體需要如何實(shí)現(xiàn)吧!
基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 實(shí)現(xiàn)的后臺(tái)管理系統(tǒng) + 用戶小程序,支持 RBAC 動(dòng)態(tài)權(quán)限、多租戶、數(shù)據(jù)權(quán)限、工作流、三方登錄、支付、短信、商城等功能
如何實(shí)現(xiàn)?
把上面的設(shè)計(jì)簡(jiǎn)單地、直白地翻譯成代碼如下:
如果你讀過(guò)我之前分析 ThreadLocal 的文章,應(yīng)該可以很容易的理解上面的操作。
這樣雖然可以實(shí)現(xiàn),但是可操作性太差,耦合性太高。
所以我們得想想怎么優(yōu)化一下,其實(shí)有個(gè)設(shè)計(jì)模式就很合適,那就是裝飾器模式。
我們可以自己搞一個(gè) Runnable 類,比如 YesRunnable,然后在 new YesRunnable 的時(shí)候,在構(gòu)造器里面把當(dāng)前線程的 threadlocal 賦值進(jìn)去。
然后 run 方法那里也修飾一下,我們直接看看偽代碼:
publicYesRunnable(Runnablerunable){
this.threadlocalCopy=copyFatherThreadlocal();
this.runable=runable;
}
publicvoidrun(){
//塞入父threadlocal,并返回當(dāng)前線程原先threadlocal
Object backup = setThreadlocal(threadlocalCopy);
try{
runable.run();//執(zhí)行被裝飾的任務(wù)邏輯
}finally{
restore(backup);//復(fù)原當(dāng)前線程的上下文
}
}
使用方式如下:
Runnabletask=()->{...};
YesRunnableyesRunnable=newYesRunnable(task);
executorService.submit(yesRunnable);
你看,這不就實(shí)現(xiàn)我們上面的設(shè)計(jì)了嘛!
不過(guò)還有一個(gè)點(diǎn)沒(méi)有揭秘,就是如何實(shí)現(xiàn) copyFatherThreadlocal
。
我們?nèi)绾蔚弥妇€程現(xiàn)在到底有哪些 Threadlocal?并且哪些是需要上下文傳遞的?
所以我們還需要?jiǎng)?chuàng)建一個(gè)類來(lái)繼承 Threadlocal。
比如叫 YesThreadlocal,用它聲明的變量就表明需要父子傳遞的!
publicclassYesThreadlocal<T>extendsThreadLocal<T>
然后我們需要搞個(gè)地方來(lái)存儲(chǔ)當(dāng)前父線程上下文用到的所有 YesThreadlocal,這樣在 copyFatherThreadlocal
的時(shí)候我們才好遍歷復(fù)制對(duì)吧?
我們可以搞個(gè) holder 來(lái)保存這些 YesThreadlocal ,不過(guò) holder 變量也得線程隔離。
畢竟每個(gè)線程所要使用的 YesThreadlocal 都不一樣,所以需要用 ThreadLocal 來(lái)修飾 holder 。
然后 YesThreadlocal 可能會(huì)有很多,我們可以用 set 來(lái)保存。
但是為了防止我們搞的這個(gè) holder 造成內(nèi)存泄漏的風(fēng)險(xiǎn),我們需要弱引用它,不過(guò)沒(méi)有 WeakHashSet,那我們就用 WeakHashMap 來(lái)替代存儲(chǔ)。
privatestaticfinalThreadLocal,?>>holder=new.....
這樣我們就打造了一個(gè)變量,它是線程獨(dú)有的,且又能拿來(lái)存儲(chǔ)當(dāng)前線程用到的所有 YesThreadLocal ,便于后面的復(fù)制,且又不會(huì)造成內(nèi)存泄漏(弱引用)。
是不是感覺(jué)有點(diǎn)暫時(shí)理不清?沒(méi)事,我們繼續(xù)來(lái)看看具體怎么用上這個(gè) hold ,可能會(huì)清晰些。
首先我們將需要傳遞給線程池的本地變量從 ThreadLocal 替換成 YesThreadLocal。
然后重寫 set 方法,實(shí)現(xiàn)如下:
@Override
publicfinalvoidset(Tvalue){
super.set(value);//調(diào)用ThreadLocal的set
addThisToHolder();//把當(dāng)前的 YesThreadLocal 對(duì)象塞入 hold 中。
}
privatevoidaddThisToHolder(){
if(!holder.get().containsKey(this)){
holder.get().put((YesThreadLocal
你看這樣就把所有用到的 YesThreadLocal 塞到 holder 中了,然后再來(lái)看看 copyFatherThreadlocal 應(yīng)該如何實(shí)現(xiàn)。
privatestaticHashMap,Object>copyFatherThreadlocal(){
HashMap,Object>fatherMap=newHashMap,Object>();
for(YesThreadLocal
邏輯很簡(jiǎn)單,就是一個(gè) map 遍歷拷貝。
我現(xiàn)在用一段話來(lái)小結(jié)一下,把上面的全部操作聯(lián)合起來(lái)理解,應(yīng)該會(huì)清晰很多。
實(shí)現(xiàn)思路小結(jié)
1.新建一個(gè) YesThreadLocal 類繼承自 ThreadLocal ,用于標(biāo)識(shí)這個(gè)修飾的變量需要父子線程拷貝
2.新建一個(gè) YesRunnable 類繼承自 Runnable,采用裝飾器模式,這樣就不用修改原有的 Runnable。在構(gòu)造階段復(fù)制父線程的 YesThreadLocal 變量賦值給 YesRunnable 的一個(gè)成員變量 threadlocalCopy 保存。
3.并修飾 YesRunnable#run 方法,在真正邏輯執(zhí)行前將 threadlocalCopy 賦值給當(dāng)前執(zhí)行線程的上下文,且保存當(dāng)前線程之前的上下文,在執(zhí)行完畢之后,再?gòu)?fù)原此線程的上下文。
4.由于需要在構(gòu)造的時(shí)候復(fù)制所有父線程用到的 YesThreadLocal ,因此需要有個(gè) holder 變量來(lái)保存所有用到的 YesThreadLocal ,這樣在構(gòu)造的時(shí)候才好遍歷賦值。
5.并且 holder 變量也需要線程隔離,所以用 ThreadLocal 修飾,并且為了防止 holder 強(qiáng)引用導(dǎo)致內(nèi)存泄漏,所以用 WeakHashMap 存儲(chǔ)。
6.往 holder 添加 YesThreadLocal 的時(shí)機(jī)就在 YesThreadLocal#set 之時(shí)
TransmittableThreadLocal 的實(shí)現(xiàn)
這篇只講 TTL 核心思想(關(guān)鍵路徑),由于篇幅原因其它的不作展開(kāi),之后再寫一篇詳細(xì)的。
我上面的實(shí)現(xiàn)其實(shí)就是 TTL 的復(fù)制版,如果你理解了上面的實(shí)現(xiàn),那么接下來(lái)對(duì) TTL 介紹理解起來(lái)應(yīng)該很簡(jiǎn)單,相當(dāng)于復(fù)習(xí)了。
我們先簡(jiǎn)單看一下 TTL 的使用方式。
使用起來(lái)很簡(jiǎn)單對(duì)吧?
TTL 對(duì)標(biāo)上面的 YesThreadLocal ,差別在于它繼承的是 InheritableThreadLocal,因?yàn)檫@樣直接 new TTL 也會(huì)擁有父子線程本地變量的傳遞能力。
我們?cè)賮?lái)看看 TTL 的 get 和 set 這兩個(gè)核心操作:
可以看到 get 和 set 其實(shí)就是復(fù)用父類 ThreadLocal 的方法,關(guān)鍵就在于 addThisToHolder
,就是我上面分析的將當(dāng)前使用的 TTL 對(duì)象加到 holder 里面。
所以,在父線程賦值即執(zhí)行 set 操作之后,父線程里的 holder 就存儲(chǔ)了當(dāng)前的 TTL 對(duì)象了,即上面演示代碼的 ttl.set() 操作。
然后重點(diǎn)就移到了TtlRunnable.get
上了,根據(jù)上面的理解我們知道這里是要進(jìn)行一個(gè)裝飾的操作,這個(gè) get 代碼也比較簡(jiǎn)單,核心就是 new 一個(gè) TtlRunnable 包裝了原始的 task。
那我們來(lái)看一下它的構(gòu)造方法:
這個(gè) capturedRef 其實(shí)就是父線程本地變量的拷貝,然后 capture()
其實(shí)就等同于copyFatherThreadlocal()
再來(lái)看一下 TtlRunnable 裝飾的 run 方法:
邏輯很清晰的四步驟:
- 拿到父類本地變量拷貝
- 賦值給當(dāng)前線程(線程池內(nèi)的某線程),并保存之前的本地變量
- 執(zhí)行邏輯
- 復(fù)原當(dāng)前線程之前的本地變量
我們?cè)賮?lái)分析一下 capture()
方法,即如何拷貝的。
在 TTL 中是專門定義了一個(gè)靜態(tài)工具類 Transmitter 來(lái)實(shí)現(xiàn)上面的 capture、 replay、restore 操作。
可以看到 capture 的邏輯其實(shí)就是返回一個(gè)快照,而這個(gè)快照就是遍歷 holder 獲取所有存儲(chǔ)在 holder 里面的 TTL ,返回一個(gè)新的 map,還是很簡(jiǎn)單的吧!
這里還有個(gè) captureThreadLocalValues ,這個(gè)是為兼容那些無(wú)法將 ThreadLocal 類變更至 TTL ,但是又想復(fù)制傳遞 ThreadLocal 的值而使用的,可以先忽略。
我們?cè)賮?lái)看看 replay,即如何將父類的本地變量賦值給當(dāng)前線程的。
邏輯還是很清晰的,先備份,再拷貝覆蓋,最后會(huì)返回備份,拷貝覆蓋的代碼 setTtlValuesTo
很簡(jiǎn)單:
就是 for 循環(huán)進(jìn)行了一波 set ,從這里也可以得知為什么上面需要移除父線程沒(méi)有的 TTL,因?yàn)檫@里只是進(jìn)行了 set。如果不 remove 當(dāng)前線程的本地變量,那就不是完全繼承自父線程的本地變量了,可能摻雜著之前的本地變量,也就是不干凈了,防止這種干擾,所以還是 remove 了為妙。
最后我們看下 restore 操作:
至此想必對(duì) TTL 的原理應(yīng)該都很清晰了吧!
一些用法
上面我們展示的只是其中一個(gè)用法也就是利用 TtlRunnable.get
來(lái)包裝 Runnable。
TTL 還提供了線程池的修飾方法,即 TtlExecutors,比如可以這樣使用:
ExecutorServiceexecutorService=TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(1));
其實(shí)原理也很簡(jiǎn)單,裝飾了一下線程池提交任務(wù)的方法,里面實(shí)現(xiàn)了 TtlRunnable.get
的包裝
還有一種使用方式更加透明,即利用 Java Agent 來(lái)修飾 JDK 的線程池實(shí)現(xiàn)類,這種方式在使用上基本就是無(wú)感知了。
在 Java 的啟動(dòng)參數(shù)加上:-javaagent:path/to/transmittable-thread-local-2.x.y.jar 即可,然后就正常的使用就行,原生的線程池實(shí)現(xiàn)類已經(jīng)悄悄的被改了!
TransmittableThreadLocalttl=newTransmittableThreadLocal<>();
ExecutorServiceexecutorService=Executors.newFixedThreadPool(1);
Runnabletask=newRunnableTask();
executorService.submit(task);
最后
好了,有關(guān) TTL 的原理和用法解釋的都差不多了。
總結(jié)下來(lái)的核心操作就是 CRR(Capture/Replay/Restore),拷貝快照、重放快照、復(fù)原上下文。
可能有些人會(huì)疑惑為什么需要復(fù)原,線程池的線程每次執(zhí)行的時(shí)候,如果用了 TTL 那執(zhí)行的線程都會(huì)被覆蓋上下文,沒(méi)必要復(fù)原對(duì)吧?
其實(shí)也有人向作者提了這個(gè)疑問(wèn),回答是:
- 線程池滿了且線程池拒絕策略使用的是『CallerRunsPolicy』,這樣執(zhí)行的線程就變成當(dāng)前線程了,那肯定是要復(fù)原的,不然上下文就沒(méi)了。
- 使用ForkJoinPool(包含并行執(zhí)行Stream與CompletableFuture,底層使用ForkJoinPool)的場(chǎng)景,展開(kāi)的ForkJoinTask會(huì)在調(diào)用線程中直接執(zhí)行。
其實(shí)關(guān)于 TTL 還有很多細(xì)節(jié)可以說(shuō),不過(guò)篇幅有限,細(xì)節(jié)要說(shuō)的話得再開(kāi)一章。不過(guò)今天這篇也算把 TTL 的核心思想講完了。
假設(shè)現(xiàn)在有個(gè)面試官問(wèn)你,我要向線程池里面?zhèn)鬟f ThreadLocal 怎么實(shí)現(xiàn)呀?想必你肯定可以回答出來(lái)了~
-
數(shù)據(jù)
+關(guān)注
關(guān)注
8文章
6812瀏覽量
88743 -
代碼
+關(guān)注
關(guān)注
30文章
4722瀏覽量
68236 -
線程
+關(guān)注
關(guān)注
0文章
503瀏覽量
19636
原文標(biāo)題:ThreadLocal的短板,我TTL來(lái)補(bǔ)!
文章出處:【微信號(hào):芋道源碼,微信公眾號(hào):芋道源碼】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論