TransmittableThreaLocal 的原理和经常使用 一文彻底搞懂阿里开源

  • 电脑网络维修
  • 2024-11-15

当天来聊一聊阿里的 TTL 也就是TransmittableThreadLocal。

关于成功父子线程的传参经常使用的普通就是InheritableThreadLocal,关于 InheritableThreadLocal 是如何成功的父子传参可以参考之前宣布的 这篇文章 。

有的同窗就会问了,既然有了InheritableThreadLocal能够成功父子线程的传参,那么阿里为什么还要在开源一个自己的TransmittableThreadLocal进去呢?

上方就说一下TransmittableThreadLocal处置了什么疑问?

版本:TransmittableTreadLocal v2.14.5

代码示例中都没有做remove操作,实践经常使用中不要遗记哦。本文代码示例参与remove方法不影响测试结果。

一、TransmittableThreadLocal处置了什么疑问?

先思索一个疑问,在业务开发中,假构想异步口头这个义务可以经常使用哪些方式?

上述的几种方式中,临时只讨论线程的方式,MQ等其余方式暂不在本文的讨论范畴内。

不论是经常使用@Async注解,还是经常使用线程或许线程池,底层原理都是经过另一个子线程口头的。

关于@Async注解原理不了解的点击链接跳转启动查阅。

《 一文搞懂 @Async 注解原理 》

既然是子线程,那么在触及到父子线程之间变量传参的时刻你们是经过什么方式成功的呢?

父子线程之间启动变量的传递可以经过InheritableThreadLocal成功。

InheritableThreadLocal成功父子线程传参的原理可以参考这篇。

《 InheritableThreadLocal 是如何成功的父子线程部分变量的传递 》

本文可以说是对InheritableThreadLocal的一个补充。

当咱们在经常使用new Thread()时,间接经过设置一个ThreadLocal即可成功变量的传递。

须要留意的是,此处传值须要经常使用InheritableThreadLocal,由于ThreadLocal不可实如今子线程中失掉到父线程的值。

由于上班中大部分场景都是经常使用的线程池,所以咱们上方的方式还可以失效吗?

线程池中线程的数量是可以指定的,并且线程是由线程池创立好,池化之后重复经常使用的。所以此时的父子线程相关中的变量传递就没有了意义,咱们须要的是义务提交到线程池时的ThreadLocal变量值传递到义务口头时的线程。

在InheritableThreadLocal原理这篇文章的末尾,咱们提到了线程池的传参方式,实质上也是经过InheritableThreadLocal启动的变量传递。

而阿里的TransmittableThreadLocal类是承袭增强的InheritableThreadLocal。

TransmittableThreadLocal可以处置线程池中复用线程时,将值传递给实践口头业务的线程,处置异步口头时的高低文传递疑问。

除此之外,还有几个典型场景例子:

二、TransmittableThreadLocal 怎样用?

上方咱们知道了TransmittableThreadLocal可以用来做什么,处置的是线程池中池化线程复用线程时的值传递疑问。

上方咱们就一同来看下怎样经常使用?

1.ThreadLocal

一切代码示例都在 springboot 中演示。

ThreadLocal 在父子线程间是如法传参的,经常使用方式如下:

@RestController@RequestMapping("/test2")public class Test2Controller {ThreadLocal<String> stringThreadLocal = new ThreadLocal<>();@RequestMapping("/set")public Object set(){stringThreadLocal.set("主线程给的值:stringThreadLocal");Thread thread = new Thread(() -> {System.out.println("读取父线程stringThreadLocal的值:" + stringThreadLocal.get());});thread.start();return "";}}

启动之后访问 /test2/set,显示如下:

经过上方的输入可以看进去,并没有读取到父线程的值。

所认为了成功父子传参,须要把 ThreadLocal 修正为 InheritableThreadLocal 。

2.InheritableThreadLocal

代码修正成功之后如下:

@RestController@RequestMapping("/test2")public class Test2Controller {ThreadLocal<String> stringThreadLocal = new ThreadLocal<>();ThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();@RequestMapping("/set")public Object set(){stringThreadLocal.set("主线程给的值:stringThreadLocal");inheritableThreadLocal.set("主线程给的值:inheritableThreadLocal");Thread thread = new Thread(() -> {System.out.println("读取父线程stringThreadLocal的值:" + stringThreadLocal.get());System.out.println("读取父线程inheritableThreadLocal的值:" + inheritableThreadLocal.get());});thread.start();return "";}}

雷同的口头一下看输入:

在上方的演示例子中,都是间接用的new Thread(),上方咱们改为线程池的方式试试。

修正成功之后的代码如下所示:

@RestController@RequestMapping("/test2")public class Test2Controller {ThreadLocal<String> stringThreadLocal = new ThreadLocal<>();ThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();ThreadLocal<String> transmittableThreadLocal = new TransmittableThreadLocal<>();ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 5, 60,TimeUnit.SECONDS, new LinkedBlockingQueue<>());@RequestMapping("/set")public Object set(){for (int i = 0; i < 10; i++) {String val = "主线程给的值:inheritableThreadLocal:"+i;System.out.println("主线程set;"+val);inheritableThreadLocal.set(val);executor.execute(()->{System.out.println("线程池:读取父线程 inheritableThreadLocal 的值:" + inheritableThreadLocal.get());});}return "";}}

雷同的看下输入:

经过输入咱们可以得出论断,当经常使用线程池时,由于线程都是复用的,在子线程中失掉父线程的值,或许失掉进去的是上一个线程 的值,所以这里会有线程安保疑问。

线程池中的线程并不肯定每次都是新创立的,所以关于InheritableThreadLocal是不可成功父子传参的。

假设觉得输入不够显著可以输入子线程的线程称号。

上方咱们看下怎样经常使用 TransmittableThreadLocal处置线程池中父子变量传递疑问。

3.TransmittableThreadLocal

继续对上方代码启动变革,变革成功之后如下所示:

修正部分:TransmittableThreadLocal 的第一种经常使用方式,TtlRunnable.get() 封装。

@RestController@RequestMapping("/test2")public class Test2Controller {ThreadLocal<String> transmittableThreadLocal = new TransmittableThreadLocal<>();ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 5, 60,TimeUnit.SECONDS, new LinkedBlockingQueue<>());@RequestMapping("/set")public Object set(){for (int i = 0; i < 10; i++) {String val = "主线程给的值:TransmittableThreadLocal:"+i;System.out.println("主线程set3;"+val);transmittableThreadLocal.set(val);executor.execute(TtlRunnable.get(()->{System.out.println("线程池线程:"+Thread.currentThread().getName()+"读取父线程 TransmittableThreadLocal 的值:"+ transmittableThreadLocal.get());}));}return "";}}

口头结果如下所示:

经过日志输入可以看到,子线程的输入曾经把父线程中设置的值所有输入了,并没有像 InheritableThreadLocal 那样不时经常使用那几个值。

可以得出论断,TransmittableThreadLocal可以处置线程池中复用线程时,将值传递给实践口头业务的线程,处置异步口头时的高低文传递疑问。

那么这样就没疑问了吗,看起来经常使用真的很繁难,仅仅须要将 Runnable 封装下即可,上方咱们将ThreadLocal中存储的 String 类型的值改为 Map在试试。

三、TransmittableThreadLocal 中的深拷贝

咱们将 ThreadLocal 中存储的值改为 Map,修正完代码如下:

@RestController@RequestMapping("/test2")public class Test2Controller {ThreadLocal<Map<String,Object>> transmittableThreadLocal = new TransmittableThreadLocal<>();ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 5, 60,TimeUnit.SECONDS, new LinkedBlockingQueue<>());@RequestMapping("/set")public Object set(){Map<String, Object> map = new HashMap<>();map.put("mainThread","主线程给的值:main");System.out.println("主线程赋值:"+ map);transmittableThreadLocal.set(map);executor.execute(TtlRunnable.get(()->{System.out.println("线程池线程:"+Thread.currentThread().getName()+"读取父线程 TransmittableThreadLocal 的值:"+ transmittableThreadLocal.get());}));return "";}}

调用接口口头结果如下:

可以看到没啥疑问,上方咱们繁难改一下代码。

修正成功的代码如下所示:

@RestController@RequestMapping("/test2")public class Test2Controller {ThreadLocal<Map<String, Object>> transmittableThreadLocal = new TransmittableThreadLocal<>();ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 5, 60,TimeUnit.SECONDS, new LinkedBlockingQueue<>());@RequestMapping("/set")public Object set(){Map<String, Object> map = transmittableThreadLocal.get();if (null == map) {map = new HashMap<>();}map.put("mainThread", "主线程给的值:main");System.out.println("主线程赋值:" + map);transmittableThreadLocal.set(map);executor.execute(TtlRunnable.get(() -> {System.out.println("子线程输入:" + Thread.currentThread().getName() + "读取父线程 TransmittableThreadLocal 的值:" + transmittableThreadLocal.get());Map<String, Object> childMap = transmittableThreadLocal.get();if (null == childMap){childMap = new HashMap<>();}childMap.put("childThread","子线程减少值");}));Map<String, Object> stringObjectMap = transmittableThreadLocal.get();if (null == stringObjectMap) {stringObjectMap = new HashMap<>();}stringObjectMap.put("mainThread-2", "主线程第二次赋值");transmittableThreadLocal.set(stringObjectMap);try{Thread.sleep(1000);}catch (InterruptedException e){e.printStackTrace();}System.out.println("主线程第二次输入ThreadLocal:"+transmittableThreadLocal.get());return "";}}

调用接口输入如下:

经过日志输入可以得出论断,当 ThreadLocal 存储的是对象时,父子线程共享同一个对象。

也就是说父子线程之间的修正都是可见的,要素就是父子线程持有的 Map 都是同一个,在父线程第二次设置值的时刻,由于修正的都是同一个 Map,所以子线程也可以读取到。

这一点须要特意的留意,假设有严厉的业务逻辑,且共享同一个ThreadLocal,须要留意这个线程安保疑问。

那么怎样处置呢,那就是深拷贝,对象的深拷贝,保障父子线程独立,在修正的时刻就不会出现父子线程共享同一个对象的事情。

TransmittableThreadLocal 其中有一个 copy 方法,copy 方法就是复制父线程值的,在此处前往一个新的对象,而不是父线程的对象即可,代码修正如下:

为什么是 copy 方法,后文会有引见。

@RestController@RequestMapping("/test2")public class Test2Controller {ThreadLocal<Map<String, Object>> transmittableThreadLocal = new TransmittableThreadLocal(){@Overridepublic Object copy(Object parentValue) {return new HashMap<>((Map)parentValue);}};ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 5, 60,TimeUnit.SECONDS, new LinkedBlockingQueue<>());@RequestMapping("/set")public Object set(){Map<String, Object> map = transmittableThreadLocal.get();if (null == map) {map = new HashMap<>();}map.put("mainThread", "主线程给的值:main");System.out.println("主线程赋值:" + map);transmittableThreadLocal.set(map);executor.execute(TtlRunnable.get(() -> {System.out.println("子线程输入:" + Thread.currentThread().getName() + "读取父线程 TransmittableThreadLocal 的值:" + transmittableThreadLocal.get());Map<String, Object> childMap = transmittableThreadLocal.get();if (null == childMap){childMap = new HashMap<>();}childMap.put("childThread","子线程减少值");}));Map<String, Object> stringObjectMap = transmittableThreadLocal.get();if (null == stringObjectMap) {stringObjectMap = new HashMap<>();}stringObjectMap.put("mainThread-2", "主线程第二次赋值");transmittableThreadLocal.set(stringObjectMap);try{Thread.sleep(1000);}catch (InterruptedException e){e.printStackTrace();}System.out.println("主线程第二次输入ThreadLocal:"+transmittableThreadLocal.get());return "";}}

修正部分如下:

调用接口,检查口头结果可以发现,父子线程的修正曾经是独立的对象在修正,不再是共享的。

置信到了这,关于 TransmittableThreadLocal 如何经常使用应该会了吧,上方咱们就一同来看下 TransmittableThreadLocal究竟是如何做到的父子线程变量的传递的。

四、TransmittableThreadLocal 原理

TransmittableThreadLocal 简称 TTL。

在开局之前先放一张官方的时序图,联合图看源码更容易懂哦!

1.TransmittableThreadLocal 经常使用方式

(1) 润色 Runnable 和Callable

这种方式就是上方代码示例中的方式,经过 TtlRunnable和TtlCallable 修正传入线程池的 Runnable 和 Callable。

(2) 润色线程池

润色线程池可以经常使用TtlExecutors工具类成功,其中有如下方法可以经常使用。

(3) Java Agent

Agent 的方式不会对代码入侵,详细的经常使用可以参考官方,这里就不再说了,官方链接我会放在文章末尾。

须要留意的是,假设须要和其余 Agent (如Skywalking、Promethues)一同经常使用,须要把 TransmittableThreadLocal Java Agent 放在第一位。

2.源码剖析

先繁难的概括下:

(1) TtlRunnable#run 方法做了什么

先从TtlRunnable#run方法入手。

从全体流程来看,整个高低文的传递流程可以规范成快照、回放、复原(CRR)三个操作。

(2) captured 快照是什么时刻做的

同窗们思索下,快照又是什么时刻做的呢?

经过上方 run 方法可以看到,在该方法的第一行曾经是失掉快照的值了,所以生成快照必需不在run方法内了。

揭示一下,扫尾放的时序图还记得吗,可以看下4.1。

还记得咱们封装了线程吗,经常使用TtlRunnable.get()启动封装的,前往的是TtlRunnable。

答案就在这个方法外部,来看下方法外部做了哪些事情。

@Nullable@Contract(value = "null -> null; !null -> !null", pure = true)public static TtlRunnable get(@Nullable Runnable runnable) {return get(runnable, false, false);}@Nullable@Contract(value = "null, _, _ -> null; !null, _, _ -> !null", pure = true)public static TtlRunnable get(@Nullable Runnable runnable, boolean releaseTtlValueReferenceAfterRun, boolean idempotent) {if (runnable == null) return null;if (runnable instanceof TtlEnhanced) {// avoid redundant decoration, and ensure idempotencyif (idempotent) return (TtlRunnable) runnable;else throw new IllegalStateException("Already TtlRunnable!");}return new TtlRunnable(runnable, releaseTtlValueReferenceAfterRun);}private TtlRunnable(@NonNull Runnable runnable, boolean releaseTtlValueReferenceAfterRun) {this.capturedRef = new AtomicReference<>(capture());this.runnable = runnable;this.releaseTtlValueReferenceAfterRun = releaseTtlValueReferenceAfterRun;}

可以看到在调用TtlRunnable.get() 方法的最后,调用了TtlRunnable的结构方法,在该方法外部,又调用了capture方法。

capture 方法外部是真正做快照的中央。

其中的transmittee.capture()调用的ttlTransmittee的。

须要留意的是,threadLocal.copyValue()拷贝的是援用,所以假设是对象,就须要重写copy方法。

public T copy(T parentValue) {return parentValue;}

代码中的 holder 是一个InheritableThreadLocal,他的值类型是WeakHashMap。

key 是TransmittableThreadLocal,value 一直是 null且一直没有经常使用。

外面保养了一切经常使用到的 TransmittableThreadLocal,一致减少到 holder中。

到了这又有了一个不懂?holder 中的 值什么时刻减少的?

堕入看源码的误区,一个一个的来,不要一个方法不时分散,要有一条主线,关于咱们这里,曾经知道了什么时刻启动的快照,如何快照的就可以了,关于 holder中的值在哪里减少的,这就是另一个疑问了。

(3) holder 中在哪赋值的

holder 中赋值的中央在 addThisToHolder方法中成功。

详细可以在transmittableThreadLocal.get()与transmittableThreadLocal.set()中检查。

@Overridepublic final T get() {T value = super.get();if (disableIgnoreNullValueSemantics || value != null) addThisToHolder();return value;}@Overridepublic final void set(T value) {if (!disableIgnoreNullValueSemantics && value == null) {// may set null to remove valueremove();} else {super.set(value);addThisToHolder();}}private void addThisToHolder() {if (!holder.get().containsKey(this)) {holder.get().put((TransmittableThreadLocal<Object>) this, null); // WeakHashMap supports null value.}}

addThisToHolder 中将此 TransmittableThreadLocal实例减少到 holder 的 key 中。

经过此方法,可以将一切用到的 TransmittableThreadLocal 实例记载。

(4) replay 备份与回放数据

replay方法只做了两件事。

在 transmittee.replay 方法中真正的口头了备份与回放操作。

(5) restore 复原

咱们看下 CRR 操作的最后一步 restore 复原。

restore 的配置就是将线程的 TTL 复原到方法口头前备份的值。

restore 方法外部调用了transmittee.restore方法。

思索一下:为什么要在义务口头完结之后口头 restore 操作呢?

首先就是为了坚持线程的洁净,线程池中的线程都是复用的。

当一个线程重复口头多个义务的时刻,第一个义务修正了 TTL 的值,假设不启动 restore ,第二个义务开局时就会失掉到第一个义务修正之后的值,而不是预期的初始的值。

五、TransmittableThreadLocal的初始化方法

关于TransmittableThreadLocal相关的初始化方法有三个,如图所示。

1.ThreadLocal#initialValue()

ThreadLocal 没有值时取值的方法,该方法在ThreadLocal#get 触发。

须要留意的是ThreadLocal#initialValue()是懒加载的,也就是创立ThreadLocal实例的时刻并不会触发ThreadLocal#initialValue()的调用。

假设咱们先启动了 ThreadLocal.set(T)操作,在启动取值操作,也不会触发ThreadLocal#initialValue(),由于曾经有值了,即使是设置的NULL也不会触发该初始化操作。

假设调用了remove 方法,在取值会触发初始化ThreadLocal#initialValue()操作。

2.InheritableThreadLocal#childValue(T)

childValue方法用于在创立新线程时,初始化子线程的InheritableThreadLocal值。

3.TransmittableThreadLocal#copy(T)

在TtlRunnable或许TtlCallable 创立的时刻触发。

例如 TtlRunnable.get()快照时触发。

用于初始化在例如:TtlRunnable口头中的TransmittableThreadLocal值。

六、总结

本文经过代码示例依次演示ThreadLocal,InheritableThreadLocal,TransmittableThreadLocal成功父子线程传参演变环节。

得出论断如下:

须要留意的是TransmittableThreadLocal保留对象时有深拷贝需求的须要重写TransmittableThreadLocal#copy(T)方法。

  • 关注微信

本网站的文章部分内容可能来源于网络和网友发布,仅供大家学习与参考,如有侵权,请联系站长进行删除处理,不代表本网站立场,转载联系作者并注明出处:https://duobeib.com/diannaowangluoweixiu/8743.html

猜你喜欢

热门标签

洗手盆如何疏浚梗塞 洗手盆为何梗塞 iPhone提价霸占4G市场等于原价8折 明码箱怎样设置明码锁 苏泊尔电饭锅保修多久 长城画龙G8253YN彩电输入指令画面变暗疑问检修 彩星彩电解除童锁方法大全 三星笔记本培修点上海 液晶显示器花屏培修视频 燃气热水器不热水要素 热水器不上班经常出现3种处置方法 无氟空调跟有氟空调有什么区别 norltz燃气热水器售后电话 大连站和大连北站哪个离周水子机场近 热水器显示屏亮显示温度不加热 铁猫牌保险箱高效开锁技巧 科技助力安保无忧 创维8R80 汽修 a1265和c3182是什么管 为什么电热水器不能即热 标致空调为什么不冷 神舟培修笔记本培修 dell1420内存更新 青岛自来水公司培修热线电话 包头美的洗衣机全国各市售后服务预定热线号码2024年修缮点降级 创维42k08rd更新 空调为什么运转异响 热水器为何会漏水 该如何处置 什么是可以自己处置的 重庆华帝售后电话 波轮洗衣机荡涤价格 鼎新热水器 留意了!不是水平疑问! 马桶产生了这5个现象 方便 极速 邢台空调移机电话上门服务 扬子空调缺点代码e4是什么疑问 宏基4736zG可以装置W11吗 奥克斯空调培修官方 为什么突然空调滴水很多 乐视s40air刷机包 未联络视的提高方向 官网培修 格力空调售后电话 皇明太阳能电话 看尚X55液晶电视进入工厂形式和软件更新方法 燃气热水器缺点代码

热门资讯

关注我们

微信公众号