今天来聊一聊阿里的
TTL
也就是
TransmittableThreadLocal
。
对于实现父子线程的传参使用的一般就是
InheritableThreadLocal
,对于
InheritableThreadLocal
是如何实现的父子传参可以参考之前发表的这篇文章。
InheritableThreadLocal 是如何实现的父子线程局部变量的传递
有的同学就会问了,既然有了
InheritableThreadLocal
能够实现父子线程的传参,那么阿里为什么还要在开源一个自己的
TransmittableThreadLocal
出来呢?
下面就说一下
TransmittableThreadLocal
解决了什么问题?
版本:
TransmittableTreadLocal v2.14.5
代码示例中都没有做
remove
操作,实际使用中不要忘记哦。本文代码示例加入
remove
方法不影响测试结果。
代码示例中都没有做
remove
操作,实际使用中不要忘记哦。本文代码示例加入
remove
方法不影响测试结果。
代码示例中都没有做
remove
操作,实际使用中不要忘记哦。本文代码示例加入
remove
方法不影响测试结果。
一、
TransmittableThreadLocal
解决了什么问题?
先思考一个问题,在业务开发中,如果想异步执行这个任务可以使用哪些方式?
- 使用
@Async
注解 new Thread()
- 线程池
- MQ
- 其它
上述的几种方式中,暂时只探讨线程的方式,MQ等其他方式暂不在本文的探讨范围内。
不管是使用
@Async
注解,还是使用线程或者线程池,底层原理都是通过另一个子线程执行的。
对于@Async注解原理不了解的点击链接跳转进行查阅。
一文搞懂@Async注解原理
既然是子线程,那么在涉及到父子线程之间变量传参的时候你们是通过什么方式实现的呢?
父子线程之间进行变量的传递可以通过
InheritableThreadLocal
实现。
InheritableThreadLocal
实现父子线程传参的原理可以参考这篇。
InheritableThreadLocal 是如何实现的父子线程局部变量的传递
本文可以说是对
InheritableThreadLocal
的一个补充。
当我们在使用
new Thread()
时,直接通过设置一个
ThreadLocal
即可实现变量的传递。
需要注意的是,此处传值需要使用
InheritableThreadLocal
,因为
ThreadLocal
无法实现在子线程中获取到父线程的值。
由于工作中大部分场景都是使用的线程池,所以我们上面的方式还可以生效吗?
线程池中线程的数量是可以指定的,并且线程是由线程池创建好,池化之后反复使用的。所以此时的父子线程关系中的变量传递就没有了意义,我们需要的是**任务提交到线程池时的
ThreadLocal
变量值传递到任务执行时的线程**。
在
InheritableThreadLocal
原理这篇文章的末尾,我们提到了线程池的传参方式,本质上也是通过
InheritableThreadLocal
进行的变量传递。
而阿里的
TransmittableThreadLocal
类是继承加强的
InheritableThreadLocal
。
**
TransmittableThreadLocal
可以解决线程池中复用线程时,将值传递给实际执行业务的线程,解决异步执行时的上下文传递问题。**
除此之外,还有几个典型场景例子:
1、分布式跟踪系统或者全链路压测(链路打标)。
2、日志收集系统上下文。
3、Session 级 Cache。
4、应用容器或者上层框架跨应用代码给下层 SDK 传递信息。
二、
TransmittableThreadLocal
怎么用?
上面我们知道了
TransmittableThreadLocal
可以用来做什么,解决的是线程池中池化线程复用线程时的值传递问题。
下面我们就一起来看下怎么使用?
2.1、ThreadLocal
所有代码示例都在 springboot 中演示。
ThreadLocal 在父子线程间是如法传参的,使用方式如下:
@RestController@RequestMapping("/test2")publicclassTest2Controller{ThreadLocal<String> stringThreadLocal =newThreadLocal<>();@RequestMapping("/set")publicObjectset(){
stringThreadLocal.set("主线程给的值:stringThreadLocal");Thread thread =newThread(()->{System.out.println("读取父线程stringThreadLocal的值:"+ stringThreadLocal.get());});
thread.start();return"";}}
启动之后访问
/test2/set
,显示如下:
通过上面的输出可以看出来,并没有读取到父线程的值。
所以为了实现父子传参,需要把
ThreadLocal
修改为
InheritableThreadLocal
。
2.2、
InheritableThreadLocal
代码修改完成之后如下:
@RestController@RequestMapping("/test2")publicclassTest2Controller{ThreadLocal<String> stringThreadLocal =newThreadLocal<>();ThreadLocal<String> inheritableThreadLocal =newInheritableThreadLocal<>();@RequestMapping("/set")publicObjectset(){
stringThreadLocal.set("主线程给的值:stringThreadLocal");
inheritableThreadLocal.set("主线程给的值:inheritableThreadLocal");Thread thread =newThread(()->{System.out.println("读取父线程stringThreadLocal的值:"+ stringThreadLocal.get());System.out.println("读取父线程inheritableThreadLocal的值:"+ inheritableThreadLocal.get());});
thread.start();return"";}}
同样的执行一下看输出:
在上面的演示例子中,都是直接用的
new Thread()
,下面我们改为线程池的方式试试。
修改完成之后的代码如下所示:
@RestController@RequestMapping("/test2")publicclassTest2Controller{ThreadLocal<String> stringThreadLocal =newThreadLocal<>();ThreadLocal<String> inheritableThreadLocal =newInheritableThreadLocal<>();ThreadLocal<String> transmittableThreadLocal =newTransmittableThreadLocal<>();ThreadPoolExecutor executor =newThreadPoolExecutor(5,5,60,TimeUnit.SECONDS,newLinkedBlockingQueue<>());@RequestMapping("/set")publicObjectset(){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
解决线程池中父子变量传递问题。
2.3、
TransmittableThreadLocal
继续对上面代码进行改造,改造完成之后如下所示:
修改部分:
TransmittableThreadLocal
的第一种使用方式,
TtlRunnable.get()
封装。
@RestController@RequestMapping("/test2")publicclassTest2Controller{ThreadLocal<String> transmittableThreadLocal =newTransmittableThreadLocal<>();ThreadPoolExecutor executor =newThreadPoolExecutor(5,5,60,TimeUnit.SECONDS,newLinkedBlockingQueue<>());@RequestMapping("/set")publicObjectset(){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")publicclassTest2Controller{ThreadLocal<Map<String,Object>> transmittableThreadLocal =newTransmittableThreadLocal<>();ThreadPoolExecutor executor =newThreadPoolExecutor(5,5,60,TimeUnit.SECONDS,newLinkedBlockingQueue<>());@RequestMapping("/set")publicObjectset(){Map<String,Object> map =newHashMap<>();
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"";}}
调用接口执行结果如下:
可以看到没啥问题,下面我们简单改一下代码。
1、在主线程提交子线程的任务之后再次修改
ThreadLocal
的值。
2、在子线程中修改
ThreadLocal
的值。
修改完成的代码如下所示:
@RestController@RequestMapping("/test2")publicclassTest2Controller{ThreadLocal<Map<String,Object>> transmittableThreadLocal =newTransmittableThreadLocal<>();ThreadPoolExecutor executor =newThreadPoolExecutor(5,5,60,TimeUnit.SECONDS,newLinkedBlockingQueue<>());@RequestMapping("/set")publicObjectset(){Map<String,Object> map = transmittableThreadLocal.get();if(null== map){map =newHashMap<>();}
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 =newHashMap<>();}
childMap.put("childThread","子线程添加值");}));Map<String,Object> stringObjectMap = transmittableThreadLocal.get();if(null== stringObjectMap){
stringObjectMap =newHashMap<>();}
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")publicclassTest2Controller{ThreadLocal<Map<String,Object>> transmittableThreadLocal =newTransmittableThreadLocal(){@OverridepublicObjectcopy(Object parentValue){returnnewHashMap<>((Map)parentValue);}};ThreadPoolExecutor executor =newThreadPoolExecutor(5,5,60,TimeUnit.SECONDS,newLinkedBlockingQueue<>());@RequestMapping("/set")publicObjectset(){Map<String,Object> map = transmittableThreadLocal.get();if(null== map){map =newHashMap<>();}
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 =newHashMap<>();}
childMap.put("childThread","子线程添加值");}));Map<String,Object> stringObjectMap = transmittableThreadLocal.get();if(null== stringObjectMap){
stringObjectMap =newHashMap<>();}
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
。
在开始之前先放一张官方的时序图,结合图看源码更容易懂哦!
4.1、TransmittableThreadLocal 使用方式
- 修饰
Runnable
和Callable
。
这种方式就是上面代码示例中的形式,通过
TtlRunnable
和
TtlCallable
修改传入线程池的
Runnable
和
Callable
。
- 修饰线程池修饰线程池可以使用
TtlExecutors
工具类实现,其中有如下方法可以使用。 - Java AgentAgent 的形式不会对代码入侵,具体的使用可以参考官网,这里就不再说了,官网链接我会放在文章末尾。> 需要注意的是,如果需要和其他 Agent (如Skywalking、Promethues)一起使用,需要把 TransmittableThreadLocal Java Agent 放在第一位。
4.2、源码分析
先简单的概括下:
1、修饰 Runnable ,将主线程的 TTL 值传入到 TtlRunnable 的构造方法中。
2、将子线程的 TTL 进行备份,主线程的值设置到子线程中。
3、子线程执行业务逻辑。
4、删除子线程新增的 TTL,将备份重新设置到子线程中。
4.2.1、TtlRunnable#run 方法做了什么
先从
TtlRunnable#run
方法入手。
从整体流程来看,整个上下文的传递流程可以规范成快照、回放、恢复(CRR)三个操作。
captured
是主线程(线程A)传递的 TTL的值。backup
是子线程(线程B)中当前存在的 TTL 的值。replay
操作会将主线程中(线程A)的 TTL 的值回放到当前子线程(线程B)中,并返回回放前的 TTL 值的备份也就是上面的backup
。runnable.run()
是待执行的方法。restore
是恢复子线程(线程B)进入之时备份的 TTL 的值。因为子线程的 TTL 可能已经发生变化,所以该方法就是回滚到子线程执行replay
方法之前的 TTL 值。
4.2.2、captured 快照是什么时候做的
同学们思考下,快照又是什么时候做的呢?
通过上面
run
方法可以看到,在该方法的第一行已经是获取快照的值了,所以生成快照肯定不在
run
方法内了。
提示一下,开头放的时序图还记得吗,可以看下
4.1
。
还记得我们封装了线程吗,使用
TtlRunnable.get()
进行封装的,返回的是
TtlRunnable
。
答案就在这个方法内部,来看下方法内部做了哪些事情。
@Nullable@Contract(value ="null -> null; !null -> !null", pure =true)publicstaticTtlRunnableget(@NullableRunnable runnable){returnget(runnable,false,false);}@Nullable@Contract(value ="null, _, _ -> null; !null, _, _ -> !null", pure =true)publicstaticTtlRunnableget(@NullableRunnable runnable,boolean releaseTtlValueReferenceAfterRun,boolean idempotent){if(runnable ==null)returnnull;if(runnable instanceofTtlEnhanced){// avoid redundant decoration, and ensure idempotencyif(idempotent)return(TtlRunnable) runnable;elsethrownewIllegalStateException("Already TtlRunnable!");}returnnewTtlRunnable(runnable, releaseTtlValueReferenceAfterRun);}privateTtlRunnable(@NonNullRunnable runnable,boolean releaseTtlValueReferenceAfterRun){this.capturedRef =newAtomicReference<>(capture());this.runnable = runnable;this.releaseTtlValueReferenceAfterRun = releaseTtlValueReferenceAfterRun;}
可以看到在调用
TtlRunnable.get()
方法的最后,调用了
TtlRunnable
的构造方法,在该方法内部,又调用了
capture
方法。
capture
方法内部是真正做快照的地方。
其中的
transmittee.capture()
调用的
ttlTransmittee
的。
需要注意的是,
threadLocal.copyValue()
拷贝的是引用,所以如果是对象,就需要重写
copy
方法。
publicTcopy(T parentValue){return parentValue;}
代码中的
holder
是一个
InheritableThreadLocal
,他的值类型是
WeakHashMap
。
key
是
TransmittableThreadLocal
,value 始终是
null
且始终没有使用。
里面维护了所有使用到的
TransmittableThreadLocal
,统一添加到
holder
中。
到了这又有了一个疑问?
holder
中的 值什么时候添加的?
陷入看源码的误区,一个一个的来,不要一个方法一直扩散,要有一条主线,对于我们这里,已经知道了什么时候进行的快照,如何快照的就可以了,对于
holder
中的值在哪里添加的,这就是另一个问题了。
4.2.3、holder 中在哪赋值的
holder
中赋值的地方在
addThisToHolder
方法中实现。
具体可以在
transmittableThreadLocal.get()
与
transmittableThreadLocal.set()
中查看。
@OverridepublicfinalTget(){T value =super.get();if(disableIgnoreNullValueSemantics || value !=null)addThisToHolder();return value;}@Overridepublicfinalvoidset(T value){if(!disableIgnoreNullValueSemantics && value ==null){// may set null to remove valueremove();}else{super.set(value);addThisToHolder();}}privatevoidaddThisToHolder(){if(!holder.get().containsKey(this)){
holder.get().put((TransmittableThreadLocal<Object>)this,null);// WeakHashMap supports null value.}}
addThisToHolder
中将此
TransmittableThreadLocal
实例添加到
holder
的
key
中。
通过此方法,可以将所有用到的
TransmittableThreadLocal
实例记录。
4.2.4、replay 备份与回放数据
replay
方法只做了两件事。
1、将快照中(主线程传递)的数据设置到当前子线程中。
2、返回当前线程的 TTL 值(快照回放当前子线程之前的TTL)。
在
transmittee.replay
方法中真正的执行了备份与回放操作。
4.2.5、restore 恢复
我们看下 CRR 操作的最后一步
restore
恢复。
restore
的功能就是将当前线程的 TTL 恢复到方法执行前备份的值。
restore
方法内部调用了
transmittee.restore
方法。
**思考一下:为什么要在任务执行结束之后执行
restore
操作呢?**
首先就是为了保持线程的干净,线程池中的线程都是复用的。
当一个线程重复执行多个任务的时候,第一个任务修改了 TTL 的值,如果不进行
restore
,第二个任务开始时就会获取到第一个任务修改之后的值,而不是预期的初始的值。
五、
TransmittableThreadLocal
的初始化方法
对于
TransmittableThreadLocal
相关的初始化方法有三个,如图所示。
5.1、**
ThreadLocal#initialValue()
**
ThreadLocal
没有值时取值的方法,该方法在
ThreadLocal#get
触发。
- 需要注意的是
ThreadLocal#initialValue()
是懒加载的,也就是创建ThreadLocal
实例的时候并不会触发ThreadLocal#initialValue()
的调用。 - 如果我们先进行了
ThreadLocal.set(T)
操作,在进行取值操作,也不会触发ThreadLocal#initialValue()
,因为已经有值了,即使是设置的NULL
也不会触发该初始化操作。 - 如果调用了
remove
方法,在取值会触发初始化ThreadLocal#initialValue()
操作。
5.2、**
InheritableThreadLocal#childValue(T)
**
childValue
方法用于在创建新线程时,初始化子线程的
InheritableThreadLocal
值。
5.3、**
TransmittableThreadLocal#copy(T)
**
在
TtlRunnable
或者
TtlCallable
创建的时候触发。
例如
TtlRunnable.get()
快照时触发。
用于初始化在
例如:TtlRunnable
执行中的
TransmittableThreadLocal
值。
六、总结
本文通过代码示例依次演示
ThreadLocal
,
InheritableThreadLocal
,
TransmittableThreadLocal
实现父子线程传参演化过程。
得出结论如下:
- 使用
ThreadLocal
无法实现父子线程传参。 InheritableThreadLocal
可以实现父子传参,但是线程池场景复用线程问题无法解决。TransmittableThreadLocal
可以解决线程池复用线程的问题。
需要注意的是
TransmittableThreadLocal
保存对象时有深拷贝需求的需要重写
TransmittableThreadLocal#copy(T)
方法。
最后也欢迎你在评论区讨论交流工作中使用
TransmittableThreadLocal
时遇到了哪些坑呢?
参考链接
https://github.com/alibaba/transmittable-thread-local
https://github.com/alibaba/transmittable-thread-local/issues/201
https://github.com/alibaba/transmittable-thread-local/issues/383
https://zhuanlan.zhihu.com/p/113388946
https://zhengw-tech.com/2021/08/22/ttl/
https://juejin.cn/post/6998552093795549191
一文搞懂@Async注解原理
InheritableThreadLocal 是如何实现的父子线程局部变量的传递
版权归原作者 醉鱼Java 所有, 如有侵权,请联系我们删除。