0


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

今天来聊一聊阿里的

TTL

也就是

TransmittableThreadLocal

对于实现父子线程的传参使用的一般就是

InheritableThreadLocal

,对于

InheritableThreadLocal

是如何实现的父子传参可以参考之前发表的这篇文章。

InheritableThreadLocal 是如何实现的父子线程局部变量的传递

有的同学就会问了,既然有了

InheritableThreadLocal

能够实现父子线程的传参,那么阿里为什么还要在开源一个自己的

TransmittableThreadLocal

出来呢?

下面就说一下

TransmittableThreadLocal

解决了什么问题?

image-20241008220812701

版本:

TransmittableTreadLocal v2.14.5

代码示例中都没有做

remove

操作,实际使用中不要忘记哦。本文代码示例加入

remove

方法不影响测试结果。

代码示例中都没有做

remove

操作,实际使用中不要忘记哦。本文代码示例加入

remove

方法不影响测试结果。

代码示例中都没有做

remove

操作,实际使用中不要忘记哦。本文代码示例加入

remove

方法不影响测试结果。

一、

TransmittableThreadLocal

解决了什么问题?

先思考一个问题,在业务开发中,如果想异步执行这个任务可以使用哪些方式?

  1. 使用@Async注解
  2. new Thread()
  3. 线程池
  4. MQ
  5. 其它

上述的几种方式中,暂时只探讨线程的方式,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

,显示如下:

image-20240928153605894

通过上面的输出可以看出来,并没有读取到父线程的值

所以为了实现父子传参,需要把

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"";}}

同样的执行一下看输出:

image-20240928154047262

在上面的演示例子中,都是直接用的

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"";}}

同样的看下输出:

image-20240928162533921

通过输出我们可以得出结论,当使用线程池时,因为线程都是复用的,在子线程中获取父线程的值,可能获取出来的是上一个线程 的值,所以这里会有线程安全问题。

线程池中的线程并不一定每次都是新创建的,所以对于

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"";}}

执行结果如下所示:

image-20240928163259332

通过日志输出可以看到,子线程的输出已经把父线程中设置的值全部输出了,并没有像

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"";}}

调用接口执行结果如下:

image-20240928194955130

可以看到没啥问题,下面我们简单改一下代码。

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"";}}

调用接口输出如下:

image-20240928200124821

通过日志输出可以得出结论,当

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"";}}

修改部分如下:

image-20240928201340611

调用接口,查看执行结果可以发现,父子线程的修改已经是独立的对象在修改,不再是共享的。

相信到了这,对于

TransmittableThreadLocal

如何使用应该会了吧,下面我们就一起来看下

TransmittableThreadLocal

到底是如何做到的父子线程变量的传递的。

四、

TransmittableThreadLocal

原理

TransmittableThreadLocal

简称

TTL

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

image-20240928202751094

4.1、TransmittableThreadLocal 使用方式

  • 修饰 RunnableCallable

​ 这种方式就是上面代码示例中的形式,通过

TtlRunnable

TtlCallable

修改传入线程池的

Runnable

Callable

  • 修饰线程池修饰线程池可以使用TtlExecutors工具类实现,其中有如下方法可以使用。image-20240928203444826
  • 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

方法入手。

image-20240928211113313

从整体流程来看,整个上下文的传递流程可以规范成快照、回放、恢复(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

方法。

image-20240929224153430

capture

方法内部是真正做快照的地方。

image-20240929224351079

其中的

transmittee.capture()

调用的

ttlTransmittee

的。

image-20240929230642634

需要注意的是,

threadLocal.copyValue()

拷贝的是引用,所以如果是对象,就需要重写

copy

方法。

publicTcopy(T parentValue){return parentValue;}

代码中的

holder

是一个

InheritableThreadLocal

,他的值类型是

WeakHashMap

image-20240929231949462

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)。

image-20240928220957597

transmittee.replay

方法中真正的执行了备份与回放操作。

image-20240928222620309

4.2.5、restore 恢复

我们看下 CRR 操作的最后一步

restore

恢复。

restore

的功能就是将当前线程的 TTL 恢复到方法执行前备份的值。

image-20241003185059929

restore

方法内部调用了

transmittee.restore

方法。

image-20241003185211557

**思考一下:为什么要在任务执行结束之后执行

restore

操作呢?**

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

当一个线程重复执行多个任务的时候,第一个任务修改了 TTL 的值,如果不进行

restore

,第二个任务开始时就会获取到第一个任务修改之后的值,而不是预期的初始的值。

五、

TransmittableThreadLocal

的初始化方法

对于

TransmittableThreadLocal

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

image-20241003203404973

5.1、**

ThreadLocal#initialValue()

**

ThreadLocal

没有值时取值的方法,该方法在

ThreadLocal#get

触发。

image-20241003204104674

  • 需要注意的是ThreadLocal#initialValue()是懒加载的,也就是创建ThreadLocal实例的时候并不会触发ThreadLocal#initialValue()的调用。
  • 如果我们先进行了 ThreadLocal.set(T)操作,在进行取值操作,也不会触发ThreadLocal#initialValue(),因为已经有值了,即使是设置的NULL也不会触发该初始化操作。
  • 如果调用了remove 方法,在取值会触发初始化ThreadLocal#initialValue()操作。

5.2、**

InheritableThreadLocal#childValue(T)

**

childValue

方法用于在创建新线程时,初始化子线程的

InheritableThreadLocal

值。

image-20241003210352475

5.3、**

TransmittableThreadLocal#copy(T)

**

TtlRunnable

或者

TtlCallable

创建的时候触发。

例如

TtlRunnable.get()

快照时触发。

用于初始化在

例如:TtlRunnable

执行中的

TransmittableThreadLocal

值。

image-20241003205557926

六、总结

本文通过代码示例依次演示

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://github.com/alibaba/transmittable-thread-local/blob/master/docs/developer-guide.md#-%E6%A1%86%E6%9E%B6%E4%B8%AD%E9%97%B4%E4%BB%B6%E9%9B%86%E6%88%90ttl%E4%BC%A0%E9%80%92

https://zhuanlan.zhihu.com/p/113388946

https://zhengw-tech.com/2021/08/22/ttl/

https://juejin.cn/post/6998552093795549191

一文搞懂@Async注解原理

InheritableThreadLocal 是如何实现的父子线程局部变量的传递


本文转载自: https://blog.csdn.net/C1041067258/article/details/142788207
版权归原作者 醉鱼Java 所有, 如有侵权,请联系我们删除。

“一文彻底搞懂阿里开源TransmittableThreaLocal的原理和使用”的评论:

还没有评论