一区二区三区三上|欧美在线视频五区|国产午夜无码在线观看视频|亚洲国产裸体网站|无码成年人影视|亚洲AV亚洲AV|成人开心激情五月|欧美性爱内射视频|超碰人人干人人上|一区二区无码三区亚洲人区久久精品

0
  • 聊天消息
  • 系統(tǒng)消息
  • 評論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會員中心
創(chuàng)作中心

完善資料讓更多小伙伴認識你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

ThreadLocal的短板,我TTL來補!

jf_ro2CN3Fa ? 來源:芋道源碼 ? 2023-09-27 16:22 ? 次閱讀
加入交流群
微信小助手二維碼

掃碼添加小助手

加入工程師交流群


有小伙伴讓我再說說TransmittableThreadLocal(下邊統(tǒng)一簡稱:TTL),它是阿里開源的一個工具類,解決異步執(zhí)行時上下文傳遞的問題。

那今天就來介紹介紹 TTL,補充下 ThreadLocal 家族的短板吧。

這篇過后,ThreadLocal 就真的一網(wǎng)打盡了!

不過還是建議先看看前置篇(文末會放鏈接),不然理解起來可能有點困難。

緣由

任何一個組件的出現(xiàn)必有其緣由,知其緣由背景才能更深刻地理解它。

我們知道 ThreadLocal 的出現(xiàn)就是為了本地化線程資源,防止不必要的多線程之間的競爭。

在有些場景,當父線程 new 一個子線程的時候,希望把它的 ThreadLocal 繼承給子線程。

這時候 InheritableThreadLocal 就來了,它就是為了父子線程傳遞本地化資源而提出的。

具體的實現(xiàn)是在子線程對象被 new 的時候,即 Thread.init 的時,如果查看到父線程內(nèi)部有 InheritableThreadLocal 的數(shù)據(jù)。

那就在子 Thread 初始化的時,把父線程的 InheritableThreadLocal 拷貝給子線程。

141edfde-5cd6-11ee-939d-92fbcf53809c.png

就這樣簡單地把父線程的 ThreadLocal 數(shù)據(jù)傳遞給子線程了。

但是,這個場景只能發(fā)生在 new Thread 的時候!也就是手動創(chuàng)建線程之時!那就有個問題了,在平時我們使用的時候基本用的都是線程池。

那就麻了啊,線程池里面的線程都預(yù)創(chuàng)建好了,調(diào)用的時候就沒法直接用 InheritableThreadLocal 了。

所以就產(chǎn)生了一個需求,如何往線程池內(nèi)的線程傳遞 ThreadLocal?,JDK 的類庫沒這個功能,所以怎么搞?

只能我們自己造輪子了。

基于 Spring Boot + MyBatis Plus + Vue & Element 實現(xiàn)的后臺管理系統(tǒng) + 用戶小程序,支持 RBAC 動態(tài)權(quán)限、多租戶、數(shù)據(jù)權(quán)限、工作流、三方登錄、支付、短信、商城等功能

  • 項目地址:https://github.com/YunaiV/ruoyi-vue-pro
  • 視頻教程:https://doc.iocoder.cn/video/

如何設(shè)計?

需求已經(jīng)明確了,但是怎么實現(xiàn)呢?

平時我們用線程池的話,比如你要提交任務(wù),則使用代碼如下:

Runnabletask=newRunnable....;
executorService.submit(task);

小貼士:以下的 ThreadLocal 泛指線程本地數(shù)據(jù),不是指 ThreadLocal 這個類

這時候,我們想著把當前線程的 ThreadLocal 傳遞給線程池內(nèi)部將要執(zhí)行這個 task 的線程。

但此時我們哪知道線程池里面的哪個線程會來執(zhí)行這個任務(wù)?

所以,我們得先把當前線程的 ThreadLocal 保存到這個 task 中。

然后當線程池里的某個線程,比如線程 A 獲取這個任務(wù)要執(zhí)行的時候,看看 task 里面是否有存儲著的 ThreadLocal 。

如果存著那就把這個 ThreadLocal 放到線程 A 的本地變量里,這樣就完成了傳遞。

然后還有一步,也挺關(guān)鍵的,就是恢復(fù)線程池內(nèi)部執(zhí)行線程的上下文,也就是該任務(wù)執(zhí)行完畢之后,把任務(wù)帶來的本地數(shù)據(jù)給刪了,把線程以前的本地數(shù)據(jù)復(fù)原。

143073d4-5cd6-11ee-939d-92fbcf53809c.png

設(shè)計思路應(yīng)該已經(jīng)很明確了吧?來看看具體需要如何實現(xiàn)吧!

基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 實現(xiàn)的后臺管理系統(tǒng) + 用戶小程序,支持 RBAC 動態(tài)權(quán)限、多租戶、數(shù)據(jù)權(quán)限、工作流、三方登錄、支付、短信、商城等功能

  • 項目地址:https://github.com/YunaiV/yudao-cloud
  • 視頻教程:https://doc.iocoder.cn/video/

如何實現(xiàn)?

把上面的設(shè)計簡單地、直白地翻譯成代碼如下:

1446c508-5cd6-11ee-939d-92fbcf53809c.png

如果你讀過我之前分析 ThreadLocal 的文章,應(yīng)該可以很容易的理解上面的操作。

這樣雖然可以實現(xiàn),但是可操作性太差,耦合性太高。

所以我們得想想怎么優(yōu)化一下,其實有個設(shè)計模式就很合適,那就是裝飾器模式。

我們可以自己搞一個 Runnable 類,比如 YesRunnable,然后在 new YesRunnable 的時候,在構(gòu)造器里面把當前線程的 threadlocal 賦值進去。

然后 run 方法那里也修飾一下,我們直接看看偽代碼:

publicYesRunnable(Runnablerunable){
this.threadlocalCopy=copyFatherThreadlocal();
this.runable=runable;
}
publicvoidrun(){
//塞入父threadlocal,并返回當前線程原先threadlocal
 Object backup = setThreadlocal(threadlocalCopy);
try{
 runable.run();//執(zhí)行被裝飾的任務(wù)邏輯
}finally{
restore(backup);//復(fù)原當前線程的上下文
}
}

使用方式如下:

Runnabletask=()->{...};
YesRunnableyesRunnable=newYesRunnable(task);
executorService.submit(yesRunnable);

你看,這不就實現(xiàn)我們上面的設(shè)計了嘛!

不過還有一個點沒有揭秘,就是如何實現(xiàn) copyFatherThreadlocal

我們?nèi)绾蔚弥妇€程現(xiàn)在到底有哪些 Threadlocal?并且哪些是需要上下文傳遞的?

所以我們還需要創(chuàng)建一個類來繼承 Threadlocal。

比如叫 YesThreadlocal,用它聲明的變量就表明需要父子傳遞的!

publicclassYesThreadlocal<T>extendsThreadLocal<T>

然后我們需要搞個地方來存儲當前父線程上下文用到的所有 YesThreadlocal,這樣在 copyFatherThreadlocal的時候我們才好遍歷復(fù)制對吧?

我們可以搞個 holder 來保存這些 YesThreadlocal ,不過 holder 變量也得線程隔離。

畢竟每個線程所要使用的 YesThreadlocal 都不一樣,所以需要用 ThreadLocal 來修飾 holder 。

然后 YesThreadlocal 可能會有很多,我們可以用 set 來保存。

但是為了防止我們搞的這個 holder 造成內(nèi)存泄漏的風(fēng)險,我們需要弱引用它,不過沒有 WeakHashSet,那我們就用 WeakHashMap 來替代存儲。

privatestaticfinalThreadLocal,?>>holder=new.....

這樣我們就打造了一個變量,它是線程獨有的,且又能拿來存儲當前線程用到的所有 YesThreadLocal ,便于后面的復(fù)制,且又不會造成內(nèi)存泄漏(弱引用)。

是不是感覺有點暫時理不清?沒事,我們繼續(xù)來看看具體怎么用上這個 hold ,可能會清晰些。

首先我們將需要傳遞給線程池的本地變量從 ThreadLocal 替換成 YesThreadLocal。

然后重寫 set 方法,實現(xiàn)如下:

@Override
publicfinalvoidset(Tvalue){
super.set(value);//調(diào)用ThreadLocal的set
addThisToHolder();//把當前的 YesThreadLocal 對象塞入 hold 中。
}
privatevoidaddThisToHolder(){
if(!holder.get().containsKey(this)){
holder.get().put((YesThreadLocal)this,null);
}
}

			

你看這樣就把所有用到的 YesThreadLocal 塞到 holder 中了,然后再來看看 copyFatherThreadlocal 應(yīng)該如何實現(xiàn)。

privatestaticHashMap,Object>copyFatherThreadlocal(){
HashMap,Object>fatherMap=newHashMap,Object>();
for(YesThreadLocalthreadLocal:YesThreadLocal.holder.get().keySet()){
fatherMap.put(threadLocal,threadLocal.copyValue());
}
returnfatherMap;
}

			

邏輯很簡單,就是一個 map 遍歷拷貝。

我現(xiàn)在用一段話來小結(jié)一下,把上面的全部操作聯(lián)合起來理解,應(yīng)該會清晰很多。

實現(xiàn)思路小結(jié)

1.新建一個 YesThreadLocal 類繼承自 ThreadLocal ,用于標識這個修飾的變量需要父子線程拷貝

2.新建一個 YesRunnable 類繼承自 Runnable,采用裝飾器模式,這樣就不用修改原有的 Runnable。在構(gòu)造階段復(fù)制父線程的 YesThreadLocal 變量賦值給 YesRunnable 的一個成員變量 threadlocalCopy 保存。

3.并修飾 YesRunnable#run 方法,在真正邏輯執(zhí)行前將 threadlocalCopy 賦值給當前執(zhí)行線程的上下文,且保存當前線程之前的上下文,在執(zhí)行完畢之后,再復(fù)原此線程的上下文。

4.由于需要在構(gòu)造的時候復(fù)制所有父線程用到的 YesThreadLocal ,因此需要有個 holder 變量來保存所有用到的 YesThreadLocal ,這樣在構(gòu)造的時候才好遍歷賦值。

5.并且 holder 變量也需要線程隔離,所以用 ThreadLocal 修飾,并且為了防止 holder 強引用導(dǎo)致內(nèi)存泄漏,所以用 WeakHashMap 存儲。

6.往 holder 添加 YesThreadLocal 的時機就在 YesThreadLocal#set 之時

TransmittableThreadLocal 的實現(xiàn)

這篇只講 TTL 核心思想(關(guān)鍵路徑),由于篇幅原因其它的不作展開,之后再寫一篇詳細的。

我上面的實現(xiàn)其實就是 TTL 的復(fù)制版,如果你理解了上面的實現(xiàn),那么接下來對 TTL 介紹理解起來應(yīng)該很簡單,相當于復(fù)習(xí)了。

我們先簡單看一下 TTL 的使用方式。

1462b3a8-5cd6-11ee-939d-92fbcf53809c.png

使用起來很簡單對吧?

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

1483e7c6-5cd6-11ee-939d-92fbcf53809c.png

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

14934dce-5cd6-11ee-939d-92fbcf53809c.png

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

14aac10c-5cd6-11ee-939d-92fbcf53809c.png

所以,在父線程賦值即執(zhí)行 set 操作之后,父線程里的 holder 就存儲了當前的 TTL 對象了,即上面演示代碼的 ttl.set() 操作。

然后重點就移到了TtlRunnable.get 上了,根據(jù)上面的理解我們知道這里是要進行一個裝飾的操作,這個 get 代碼也比較簡單,核心就是 new 一個 TtlRunnable 包裝了原始的 task。

14ba6120-5cd6-11ee-939d-92fbcf53809c.png

那我們來看一下它的構(gòu)造方法:

14cc5c68-5cd6-11ee-939d-92fbcf53809c.png

這個 capturedRef 其實就是父線程本地變量的拷貝,然后 capture() 其實就等同于copyFatherThreadlocal()

再來看一下 TtlRunnable 裝飾的 run 方法:

14e6099c-5cd6-11ee-939d-92fbcf53809c.png

邏輯很清晰的四步驟:

  1. 拿到父類本地變量拷貝
  2. 賦值給當前線程(線程池內(nèi)的某線程),并保存之前的本地變量
  3. 執(zhí)行邏輯
  4. 復(fù)原當前線程之前的本地變量

我們再來分析一下 capture() 方法,即如何拷貝的。

在 TTL 中是專門定義了一個靜態(tài)工具類 Transmitter 來實現(xiàn)上面的 capture、 replay、restore 操作。

1502c258-5cd6-11ee-939d-92fbcf53809c.png

可以看到 capture 的邏輯其實就是返回一個快照,而這個快照就是遍歷 holder 獲取所有存儲在 holder 里面的 TTL ,返回一個新的 map,還是很簡單的吧!

這里還有個 captureThreadLocalValues ,這個是為兼容那些無法將 ThreadLocal 類變更至 TTL ,但是又想復(fù)制傳遞 ThreadLocal 的值而使用的,可以先忽略。

我們再來看看 replay,即如何將父類的本地變量賦值給當前線程的。

1519692c-5cd6-11ee-939d-92fbcf53809c.png

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

152bc84c-5cd6-11ee-939d-92fbcf53809c.png

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

最后我們看下 restore 操作:

153fc5a4-5cd6-11ee-939d-92fbcf53809c.png

至此想必對 TTL 的原理應(yīng)該都很清晰了吧!

一些用法

上面我們展示的只是其中一個用法也就是利用 TtlRunnable.get 來包裝 Runnable。

TTL 還提供了線程池的修飾方法,即 TtlExecutors,比如可以這樣使用:

ExecutorServiceexecutorService=TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(1));

其實原理也很簡單,裝飾了一下線程池提交任務(wù)的方法,里面實現(xiàn)了 TtlRunnable.get 的包裝

155e56c2-5cd6-11ee-939d-92fbcf53809c.png

還有一種使用方式更加透明,即利用 Java Agent 來修飾 JDK 的線程池實現(xiàn)類,這種方式在使用上基本就是無感知了。

在 Java 的啟動參數(shù)加上:-javaagent:path/to/transmittable-thread-local-2.x.y.jar 即可,然后就正常的使用就行,原生的線程池實現(xiàn)類已經(jīng)悄悄的被改了!

TransmittableThreadLocalttl=newTransmittableThreadLocal<>();
ExecutorServiceexecutorService=Executors.newFixedThreadPool(1);
Runnabletask=newRunnableTask();
executorService.submit(task);

最后

好了,有關(guān) TTL 的原理和用法解釋的都差不多了。

總結(jié)下來的核心操作就是 CRR(Capture/Replay/Restore),拷貝快照、重放快照、復(fù)原上下文。

可能有些人會疑惑為什么需要復(fù)原,線程池的線程每次執(zhí)行的時候,如果用了 TTL 那執(zhí)行的線程都會被覆蓋上下文,沒必要復(fù)原對吧?

其實也有人向作者提了這個疑問,回答是:

  • 線程池滿了且線程池拒絕策略使用的是『CallerRunsPolicy』,這樣執(zhí)行的線程就變成當前線程了,那肯定是要復(fù)原的,不然上下文就沒了。
  • 使用ForkJoinPool(包含并行執(zhí)行Stream與CompletableFuture,底層使用ForkJoinPool)的場景,展開的ForkJoinTask會在調(diào)用線程中直接執(zhí)行。

其實關(guān)于 TTL 還有很多細節(jié)可以說,不過篇幅有限,細節(jié)要說的話得再開一章。不過今天這篇也算把 TTL 的核心思想講完了。

假設(shè)現(xiàn)在有個面試官問你,我要向線程池里面?zhèn)鬟f ThreadLocal 怎么實現(xiàn)呀?想必你肯定可以回答出來了~


聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問題,請聯(lián)系本站處理。 舉報投訴
  • 數(shù)據(jù)
    +關(guān)注

    關(guān)注

    8

    文章

    7250

    瀏覽量

    91506
  • 代碼
    +關(guān)注

    關(guān)注

    30

    文章

    4893

    瀏覽量

    70443
  • 線程
    +關(guān)注

    關(guān)注

    0

    文章

    508

    瀏覽量

    20132

原文標題:ThreadLocal的短板,我TTL來補!

文章出處:【微信號:芋道源碼,微信公眾號:芋道源碼】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。

收藏 人收藏
加入交流群
微信小助手二維碼

掃碼添加小助手

加入工程師交流群

    評論

    相關(guān)推薦
    熱點推薦

    ThreadLocal實例應(yīng)用

    ThreadLocal相信大家都用過,但你知道他的原理嗎,今天了不起帶大家學(xué)習(xí)ThreadLocal。 ThreadLocal是什么 在多線程編程中,經(jīng)常會遇到需要在不同線程中共享數(shù)據(jù)的情況
    的頭像 發(fā)表于 09-30 10:19 ?866次閱讀
    <b class='flag-5'>ThreadLocal</b>實例應(yīng)用

    ThreadLocal的定義、用法及優(yōu)點

    ThreadLocal 簡介 ThreadLocal是Java中一個非常重要的線程技術(shù)。它可以讓每個線程都擁有自己的變量副本,避免了線程間的競爭和數(shù)據(jù)泄露問題。在本文中,我們將詳細介紹
    的頭像 發(fā)表于 09-30 10:14 ?1396次閱讀
    <b class='flag-5'>ThreadLocal</b>的定義、用法及優(yōu)點

    二維插

    主要任務(wù):以單片機為控制器,步進電機為執(zhí)行器,構(gòu)建一個二維插系統(tǒng),該系統(tǒng)能夠進行4個象限的直線、圓弧插。目標:設(shè)計控制系統(tǒng)硬件電路,編寫插軟件各種插功能通過按鍵
    發(fā)表于 02-08 15:47

    晶振:專家詳解溫晶振

      你對溫晶振了解嗎?或者說你了解溫晶振多少呢?下面我們跟著松季晶振全面具體的了解溫晶振?! ?、松季電子介紹說:溫晶振即溫度補償
    發(fā)表于 08-14 16:03

    LVDS與TTL轉(zhuǎn)換

    電路中實現(xiàn)的是28路TTL轉(zhuǎn)為LVDS,再轉(zhuǎn)為28路TTL,的管腳分配是根據(jù)cameralink的協(xié)議的,檢查后也沒發(fā)現(xiàn)問題,下面圖里
    發(fā)表于 08-04 22:40

    需要什么適應(yīng)TTL電平到電力線?

    你好,高興地發(fā)現(xiàn),PSoC1(CY8C29)現(xiàn)在能夠“房子”PLC IP解決方案。在我看來,這是一個非常有趣的問題,即使它是一個資源匱乏的解決方案。的問題是…需要什么適應(yīng)TTL
    發(fā)表于 05-15 08:36

    光燈的單片機開發(fā)設(shè)計

    景完全可以,只要您覺得昏暗的場景可以用它們光,關(guān)鍵同樣便宜,可以說它是日常攝影照明的理想合作伙伴!光燈有多種類型,包括環(huán)形,方形,桿形等等多種樣式!閃光燈:常見的閃光燈類型是機器頂部的熱靴閃光燈。當然
    發(fā)表于 06-29 18:00

    改進型TTL門電路—抗飽和TTL電路

    改進型TTL門電路——抗飽和TTL電路   抗飽和TTL電路是目前傳輸速度較高的一類TTL電路。這種電路由于采用肖特基勢壘二極管SBD鉗位方法
    發(fā)表于 04-07 00:16 ?2808次閱讀
    改進型<b class='flag-5'>TTL</b>門電路—抗飽和<b class='flag-5'>TTL</b>電路

    終端短板 TD-SCDMA醞釀商用化質(zhì)變

    終端短板TD醞釀商用化質(zhì)變    “終端還是現(xiàn)在最令人牽掛的部分?!毙畔a(chǎn)業(yè)部電信研究院一高層對《第一財經(jīng)日報》表示,TD-SCDMA網(wǎng)絡(luò)問題不大,而終端的成熟度、品
    發(fā)表于 06-23 09:21 ?311次閱讀

    什么是短板印刷

    什么是短板印刷 短板印刷圖片 短板印刷是
    發(fā)表于 10-13 09:29 ?3189次閱讀

    ThreadLocal發(fā)生內(nèi)存泄漏的原因

    ,就可能會導(dǎo)致內(nèi)存泄漏。下面,我們將圍繞三個方面分析 ThreadLocal 內(nèi)存泄漏的問題 ThreadLocal 實現(xiàn)原理 ThreadLocal為什么會內(nèi)存泄漏
    的頭像 發(fā)表于 05-05 16:23 ?3893次閱讀

    如何使用ThreadLocal避免內(nèi)存泄漏

    本次給大家介紹重要的工具ThreadLocal。講解內(nèi)容如下,同時介紹什么場景下發(fā)生內(nèi)存泄漏,如何復(fù)現(xiàn)內(nèi)存泄漏,如何正確使用它避免內(nèi)存泄漏。 ThreadLocal是什么?有哪些用途
    的頭像 發(fā)表于 08-20 09:29 ?4450次閱讀
    如何使用<b class='flag-5'>ThreadLocal</b><b class='flag-5'>來</b>避免內(nèi)存泄漏

    ThreadLocal源碼解析及實戰(zhàn)應(yīng)用

    ThreadLocal 是一個關(guān)于創(chuàng)建線程局部變量的類。
    的頭像 發(fā)表于 01-29 14:53 ?633次閱讀

    ThreadLocal基本內(nèi)容與用法

    下面我們就來看看道哥都用的ThreadLocal。 1 ThreadLocal你來自哪里 Since : 1.2 Author : Josh Bloch and Doug Lea 又是并發(fā)大佬們
    的頭像 發(fā)表于 10-13 11:39 ?679次閱讀

    無功補償?shù)墓?b class='flag-5'>補、分、混

    的穩(wěn)定性和可靠性。 在實際應(yīng)用中,無功補償主要分為共、分和混三種方式。 共是指在電力系統(tǒng)中通過集中式的無功補償設(shè)備補償整個系統(tǒng)的無
    的頭像 發(fā)表于 12-19 15:47 ?1559次閱讀