有小伙伴讓我再說說TransmittableThreadLocal(下邊統一簡稱:TTL),它是阿里開源的一個工具類,解決異步執行時上下文傳遞的問題。
那今天就來介紹介紹 TTL,補充下 ThreadLocal 家族的短板吧。
這篇過后,ThreadLocal 就真的一網打盡了!
不過還是建議先看看前置篇(文末會放鏈接),不然理解起來可能有點困難。
緣由
任何一個組件的出現必有其緣由,知其緣由背景才能更深刻地理解它。
我們知道 ThreadLocal 的出現就是為了本地化線程資源,防止不必要的多線程之間的競爭。
在有些場景,當父線程 new 一個子線程的時候,希望把它的 ThreadLocal 繼承給子線程。
這時候 InheritableThreadLocal 就來了,它就是為了父子線程傳遞本地化資源而提出的。
具體的實現是在子線程對象被 new 的時候,即 Thread.init 的時,如果查看到父線程內部有 InheritableThreadLocal 的數據。
那就在子 Thread 初始化的時,把父線程的 InheritableThreadLocal 拷貝給子線程。

就這樣簡單地把父線程的 ThreadLocal 數據傳遞給子線程了。
但是,這個場景只能發生在 new Thread 的時候!也就是手動創建線程之時!那就有個問題了,在平時我們使用的時候基本用的都是線程池。
那就麻了啊,線程池里面的線程都預創建好了,調用的時候就沒法直接用 InheritableThreadLocal 了。
所以就產生了一個需求,如何往線程池內的線程傳遞 ThreadLocal?,JDK 的類庫沒這個功能,所以怎么搞?
只能我們自己造輪子了。
基于 Spring Boot + MyBatis Plus + Vue & Element 實現的后臺管理系統 + 用戶小程序,支持 RBAC 動態權限、多租戶、數據權限、工作流、三方登錄、支付、短信、商城等功能
- 項目地址:https://github.com/YunaiV/ruoyi-vue-pro
- 視頻教程:https://doc.iocoder.cn/video/
如何設計?
需求已經明確了,但是怎么實現呢?
平時我們用線程池的話,比如你要提交任務,則使用代碼如下:
Runnabletask=newRunnable....;
executorService.submit(task);
小貼士:以下的 ThreadLocal 泛指線程本地數據,不是指 ThreadLocal 這個類
這時候,我們想著把當前線程的 ThreadLocal 傳遞給線程池內部將要執行這個 task 的線程。
但此時我們哪知道線程池里面的哪個線程會來執行這個任務?
所以,我們得先把當前線程的 ThreadLocal 保存到這個 task 中。
然后當線程池里的某個線程,比如線程 A 獲取這個任務要執行的時候,看看 task 里面是否有存儲著的 ThreadLocal 。
如果存著那就把這個 ThreadLocal 放到線程 A 的本地變量里,這樣就完成了傳遞。
然后還有一步,也挺關鍵的,就是恢復線程池內部執行線程的上下文,也就是該任務執行完畢之后,把任務帶來的本地數據給刪了,把線程以前的本地數據復原。

設計思路應該已經很明確了吧?來看看具體需要如何實現吧!
基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 實現的后臺管理系統 + 用戶小程序,支持 RBAC 動態權限、多租戶、數據權限、工作流、三方登錄、支付、短信、商城等功能
如何實現?
把上面的設計簡單地、直白地翻譯成代碼如下:

如果你讀過我之前分析 ThreadLocal 的文章,應該可以很容易的理解上面的操作。
這樣雖然可以實現,但是可操作性太差,耦合性太高。
所以我們得想想怎么優化一下,其實有個設計模式就很合適,那就是裝飾器模式。
我們可以自己搞一個 Runnable 類,比如 YesRunnable,然后在 new YesRunnable 的時候,在構造器里面把當前線程的 threadlocal 賦值進去。
然后 run 方法那里也修飾一下,我們直接看看偽代碼:
publicYesRunnable(Runnablerunable){
this.threadlocalCopy=copyFatherThreadlocal();
this.runable=runable;
}
publicvoidrun(){
//塞入父threadlocal,并返回當前線程原先threadlocal
Object backup = setThreadlocal(threadlocalCopy);
try{
runable.run();//執行被裝飾的任務邏輯
}finally{
restore(backup);//復原當前線程的上下文
}
}
使用方式如下:
Runnabletask=()->{...};
YesRunnableyesRunnable=newYesRunnable(task);
executorService.submit(yesRunnable);
你看,這不就實現我們上面的設計了嘛!
不過還有一個點沒有揭秘,就是如何實現 copyFatherThreadlocal
。
我們如何得知父線程現在到底有哪些 Threadlocal?并且哪些是需要上下文傳遞的?
所以我們還需要創建一個類來繼承 Threadlocal。
比如叫 YesThreadlocal,用它聲明的變量就表明需要父子傳遞的!
publicclassYesThreadlocal<T>extendsThreadLocal<T>
然后我們需要搞個地方來存儲當前父線程上下文用到的所有 YesThreadlocal,這樣在 copyFatherThreadlocal
的時候我們才好遍歷復制對吧?
我們可以搞個 holder 來保存這些 YesThreadlocal ,不過 holder 變量也得線程隔離。
畢竟每個線程所要使用的 YesThreadlocal 都不一樣,所以需要用 ThreadLocal 來修飾 holder 。
然后 YesThreadlocal 可能會有很多,我們可以用 set 來保存。
但是為了防止我們搞的這個 holder 造成內存泄漏的風險,我們需要弱引用它,不過沒有 WeakHashSet,那我們就用 WeakHashMap 來替代存儲。
privatestaticfinalThreadLocal,?>>holder=new.....
這樣我們就打造了一個變量,它是線程獨有的,且又能拿來存儲當前線程用到的所有 YesThreadLocal ,便于后面的復制,且又不會造成內存泄漏(弱引用)。
是不是感覺有點暫時理不清?沒事,我們繼續來看看具體怎么用上這個 hold ,可能會清晰些。
首先我們將需要傳遞給線程池的本地變量從 ThreadLocal 替換成 YesThreadLocal。
然后重寫 set 方法,實現如下:
@Override
publicfinalvoidset(Tvalue){
super.set(value);//調用ThreadLocal的set
addThisToHolder();//把當前的 YesThreadLocal 對象塞入 hold 中。
}
privatevoidaddThisToHolder(){
if(!holder.get().containsKey(this)){
holder.get().put((YesThreadLocal
你看這樣就把所有用到的 YesThreadLocal 塞到 holder 中了,然后再來看看 copyFatherThreadlocal 應該如何實現。
privatestaticHashMap,Object>copyFatherThreadlocal(){
HashMap,Object>fatherMap=newHashMap,Object>();
for(YesThreadLocal
邏輯很簡單,就是一個 map 遍歷拷貝。
我現在用一段話來小結一下,把上面的全部操作聯合起來理解,應該會清晰很多。
實現思路小結
1.新建一個 YesThreadLocal 類繼承自 ThreadLocal ,用于標識這個修飾的變量需要父子線程拷貝
2.新建一個 YesRunnable 類繼承自 Runnable,采用裝飾器模式,這樣就不用修改原有的 Runnable。在構造階段復制父線程的 YesThreadLocal 變量賦值給 YesRunnable 的一個成員變量 threadlocalCopy 保存。
3.并修飾 YesRunnable#run 方法,在真正邏輯執行前將 threadlocalCopy 賦值給當前執行線程的上下文,且保存當前線程之前的上下文,在執行完畢之后,再復原此線程的上下文。
4.由于需要在構造的時候復制所有父線程用到的 YesThreadLocal ,因此需要有個 holder 變量來保存所有用到的 YesThreadLocal ,這樣在構造的時候才好遍歷賦值。
5.并且 holder 變量也需要線程隔離,所以用 ThreadLocal 修飾,并且為了防止 holder 強引用導致內存泄漏,所以用 WeakHashMap 存儲。
6.往 holder 添加 YesThreadLocal 的時機就在 YesThreadLocal#set 之時
TransmittableThreadLocal 的實現
這篇只講 TTL 核心思想(關鍵路徑),由于篇幅原因其它的不作展開,之后再寫一篇詳細的。
我上面的實現其實就是 TTL 的復制版,如果你理解了上面的實現,那么接下來對 TTL 介紹理解起來應該很簡單,相當于復習了。
我們先簡單看一下 TTL 的使用方式。

使用起來很簡單對吧?
TTL 對標上面的 YesThreadLocal ,差別在于它繼承的是 InheritableThreadLocal,因為這樣直接 new TTL 也會擁有父子線程本地變量的傳遞能力。

我們再來看看 TTL 的 get 和 set 這兩個核心操作:

可以看到 get 和 set 其實就是復用父類 ThreadLocal 的方法,關鍵就在于 addThisToHolder
,就是我上面分析的將當前使用的 TTL 對象加到 holder 里面。

所以,在父線程賦值即執行 set 操作之后,父線程里的 holder 就存儲了當前的 TTL 對象了,即上面演示代碼的 ttl.set() 操作。
然后重點就移到了TtlRunnable.get
上了,根據上面的理解我們知道這里是要進行一個裝飾的操作,這個 get 代碼也比較簡單,核心就是 new 一個 TtlRunnable 包裝了原始的 task。

那我們來看一下它的構造方法:

這個 capturedRef 其實就是父線程本地變量的拷貝,然后 capture()
其實就等同于copyFatherThreadlocal()
再來看一下 TtlRunnable 裝飾的 run 方法:

邏輯很清晰的四步驟:
- 拿到父類本地變量拷貝
- 賦值給當前線程(線程池內的某線程),并保存之前的本地變量
- 執行邏輯
- 復原當前線程之前的本地變量
我們再來分析一下 capture()
方法,即如何拷貝的。
在 TTL 中是專門定義了一個靜態工具類 Transmitter 來實現上面的 capture、 replay、restore 操作。

可以看到 capture 的邏輯其實就是返回一個快照,而這個快照就是遍歷 holder 獲取所有存儲在 holder 里面的 TTL ,返回一個新的 map,還是很簡單的吧!
這里還有個 captureThreadLocalValues ,這個是為兼容那些無法將 ThreadLocal 類變更至 TTL ,但是又想復制傳遞 ThreadLocal 的值而使用的,可以先忽略。
我們再來看看 replay,即如何將父類的本地變量賦值給當前線程的。

邏輯還是很清晰的,先備份,再拷貝覆蓋,最后會返回備份,拷貝覆蓋的代碼 setTtlValuesTo
很簡單:

就是 for 循環進行了一波 set ,從這里也可以得知為什么上面需要移除父線程沒有的 TTL,因為這里只是進行了 set。如果不 remove 當前線程的本地變量,那就不是完全繼承自父線程的本地變量了,可能摻雜著之前的本地變量,也就是不干凈了,防止這種干擾,所以還是 remove 了為妙。
最后我們看下 restore 操作:

至此想必對 TTL 的原理應該都很清晰了吧!
一些用法
上面我們展示的只是其中一個用法也就是利用 TtlRunnable.get
來包裝 Runnable。
TTL 還提供了線程池的修飾方法,即 TtlExecutors,比如可以這樣使用:
ExecutorServiceexecutorService=TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(1));
其實原理也很簡單,裝飾了一下線程池提交任務的方法,里面實現了 TtlRunnable.get
的包裝

還有一種使用方式更加透明,即利用 Java Agent 來修飾 JDK 的線程池實現類,這種方式在使用上基本就是無感知了。
在 Java 的啟動參數加上:-javaagent:path/to/transmittable-thread-local-2.x.y.jar 即可,然后就正常的使用就行,原生的線程池實現類已經悄悄的被改了!
TransmittableThreadLocalttl=newTransmittableThreadLocal<>();
ExecutorServiceexecutorService=Executors.newFixedThreadPool(1);
Runnabletask=newRunnableTask();
executorService.submit(task);
最后
好了,有關 TTL 的原理和用法解釋的都差不多了。
總結下來的核心操作就是 CRR(Capture/Replay/Restore),拷貝快照、重放快照、復原上下文。
可能有些人會疑惑為什么需要復原,線程池的線程每次執行的時候,如果用了 TTL 那執行的線程都會被覆蓋上下文,沒必要復原對吧?
其實也有人向作者提了這個疑問,回答是:
- 線程池滿了且線程池拒絕策略使用的是『CallerRunsPolicy』,這樣執行的線程就變成當前線程了,那肯定是要復原的,不然上下文就沒了。
- 使用ForkJoinPool(包含并行執行Stream與CompletableFuture,底層使用ForkJoinPool)的場景,展開的ForkJoinTask會在調用線程中直接執行。
其實關于 TTL 還有很多細節可以說,不過篇幅有限,細節要說的話得再開一章。不過今天這篇也算把 TTL 的核心思想講完了。
假設現在有個面試官問你,我要向線程池里面傳遞 ThreadLocal 怎么實現呀?想必你肯定可以回答出來了~
-
數據
+關注
關注
8文章
7255瀏覽量
91811 -
代碼
+關注
關注
30文章
4900瀏覽量
70671 -
線程
+關注
關注
0文章
508瀏覽量
20203
原文標題:ThreadLocal的短板,我TTL來補!
文章出處:【微信號:芋道源碼,微信公眾號:芋道源碼】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
ThreadLocal實例應用

ThreadLocal的定義、用法及優點

二維插補
需要什么來適應TTL電平到電力線?
補光燈的單片機開發設計
改進型TTL門電路—抗飽和TTL電路

力補終端短板 TD-SCDMA醞釀商用化質變
ThreadLocal發生內存泄漏的原因
如何使用ThreadLocal來避免內存泄漏

評論