对于实现父子线程的传参使用的一般就是InheritableThreadLocal,对于 InheritableThreadLocal 是如何实现的父子传参可以参考之前发表的这篇文章。
有的同学就会问了,既然有了InheritableThreadLocal能够实现父子线程的传参,那么阿里为什么还要在开源一个自己的TransmittableThreadLocal出来呢?
下面就说一下TransmittableThreadLocal解决了什么问题?
版本:TransmittableTreadLocal v2.14.5
代码示例中都没有做remove操作,实际使用中不要忘记哦。本文代码示例加入remove方法不影响测试结果。
一、TransmittableThreadLocal解决了什么问题?
先思考一个问题,在业务开发中,如果想异步执行这个任务可以使用哪些方式?
- 使用@Async注解
- new Thread()
- 线程池
- MQ
- 其它
上述的几种方式中,暂时只探讨线程的方式,MQ等其他方式暂不在本文的探讨范围内。
不管是使用@Async注解,还是使用线程或者线程池,底层原理都是通过另一个子线程执行的。
对于@Async注解原理不了解的点击链接跳转进行查阅。
既然是子线程,那么在涉及到父子线程之间变量传参的时候你们是通过什么方式实现的呢?
父子线程之间进行变量的传递可以通过InheritableThreadLocal实现。
InheritableThreadLocal实现父子线程传参的原理可以参考这篇。
《InheritableThreadLocal 是如何实现的父子线程局部变量的传递》
本文可以说是对InheritableThreadLocal的一个补充。
当我们在使用new Thread()时,直接通过设置一个ThreadLocal即可实现变量的传递。
需要注意的是,此处传值需要使用InheritableThreadLocal,因为ThreadLocal无法实现在子线程中获取到父线程的值。
由于工作中大部分场景都是使用的线程池,所以我们上面的方式还可以生效吗?
线程池中线程的数量是可以指定的,并且线程是由线程池创建好,池化之后反复使用的。所以此时的父子线程关系中的变量传递就没有了意义,我们需要的是任务提交到线程池时的ThreadLocal变量值传递到任务执行时的线程。
在InheritableThreadLocal原理这篇文章的末尾,我们提到了线程池的传参方式,本质上也是通过InheritableThreadLocal进行的变量传递。
而阿里的TransmittableThreadLocal类是继承加强的InheritableThreadLocal。
TransmittableThreadLocal可以解决线程池中复用线程时,将值传递给实际执行业务的线程,解决异步执行时的上下文传递问题。
除此之外,还有几个典型场景例子:
- 分布式跟踪系统或者全链路压测(链路打标)。
- 日志收集系统上下文。
- Session 级 Cache。
- 应用容器或者上层框架跨应用代码给下层 SDK 传递信息。
二、TransmittableThreadLocal 怎么用?
上面我们知道了TransmittableThreadLocal可以用来做什么,解决的是线程池中池化线程复用线程时的值传递问题。
下面我们就一起来看下怎么使用?
1.ThreadLocal
所有代码示例都在 springboot 中演示。
ThreadLocal 在父子线程间是如法传参的,使用方式如下:
@RestController
@RequestMapping("/test2")
public class Test2Controller {
ThreadLocal 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 stringThreadLocal = new ThreadLocal<>();
ThreadLocal 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 stringThreadLocal = new ThreadLocal<>();
ThreadLocal inheritableThreadLocal = new InheritableThreadLocal<>();
ThreadLocal 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 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
调用接口执行结果如下:
可以看到没啥问题,下面我们简单改一下代码。
- 在主线程提交子线程的任务之后再次修改 ThreadLocal 的值。
- 在子线程中修改 ThreadLocal 的值。
修改完成的代码如下所示:
@RestController
@RequestMapping("/test2")
public class Test2Controller {
ThreadLocal
调用接口输出如下:
通过日志输出可以得出结论,当 ThreadLocal 存储的是对象时,父子线程共享同一个对象。
也就是说父子线程之间的修改都是可见的,原因就是父子线程持有的 Map 都是同一个,在父线程第二次设置值的时候,因为修改的都是同一个 Map,所以子线程也可以读取到。
这一点需要特别的注意,如果有严格的业务逻辑,且共享同一个ThreadLocal,需要注意这个线程安全问题。
那么怎么解决呢,那就是深拷贝,对象的深拷贝,保证父子线程独立,在修改的时候就不会出现父子线程共享同一个对象的事情。
TransmittableThreadLocal 其中有一个 copy 方法,copy 方法就是复制父线程值的,在此处返回一个新的对象,而不是父线程的对象即可,代码修改如下:
为什么是 copy 方法,后文会有介绍。
@RestController
@RequestMapping("/test2")
public class Test2Controller {
ThreadLocal
修改部分如下:
调用接口,查看执行结果可以发现,父子线程的修改已经是独立的对象在修改,不再是共享的。
相信到了这,对于 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.源码分析
先简单的概括下:
- 修饰 Runnable ,将主线程的 TTL 值传入到 TtlRunnable 的构造方法中。
- 将子线程的 TTL 进行备份,主线程的值设置到子线程中。
- 子线程执行业务逻辑。
- 删除子线程新增的 TTL,将备份重新设置到子线程中。
(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 值。
(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 idempotency
if (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()中查看。
@Override
public final T get() {
T value = super.get();
if (disableIgnoreNullValueSemantics || value != null) addThisToHolder();
return value;
}
@Override
public final void set(T value) {
if (!disableIgnoreNullValueSemantics && value == null) {
// may set null to remove value
remove();
} else {
super.set(value);
addThisToHolder();
}
}
private void addThisToHolder() {
if (!holder.get().containsKey(this)) {
holder.get().put((TransmittableThreadLocal
addThisToHolder 中将此 TransmittableThreadLocal实例添加到 holder 的 key 中。
通过此方法,可以将所有用到的 TransmittableThreadLocal 实例记录。
(4) replay 备份与回放数据
replay方法只做了两件事。
- 将快照中(主线程传递)的数据设置到当前子线程中。
- 返回当前线程的 TTL 值(快照回放当前子线程之前的TTL)。
在 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实现父子线程传参演化过程。
得出结论如下:
- 使用ThreadLocal无法实现父子线程传参。
- InheritableThreadLocal可以实现父子传参,但是线程池场景复用线程问题无法解决。
- TransmittableThreadLocal可以解决线程池复用线程的问题。
需要注意的是TransmittableThreadLocal保存对象时有深拷贝需求的需要重写TransmittableThreadLocal#copy(T)方法。