本文源码截取自:TransmittableThreadLocal官方开源仓库
使用版本:release - v2.11.5
最近在准备双十一压测相关的调整工作,需要将线程池改为都使用TTL包装过的,用于辅助pinpoint插件实现Trace信息传递。之前没怎么关注过TTL这个库,因此在改用TTL的同时,学习下其实现原理。
一、背景
InheritableThreadLocal及其局限性
Jdk提供了InheritableThreadLocal
类,用于在父子线程间传递线程变量(ThreadLocal),实现原理就是在Thread类保存名为inheritableThreadLocals的成员属性(以InheritableThreadLocal
对象为Key的ThreadLocalMap),并在初始化创建子线程时,将父线程的inheritableThreadLocals赋给子线程,这部分逻辑在Thread.init()
方法内。
这种方式在线程只被创建和使用一次时是有效的,但对于使用线程池的场景下,由于线程被复用,初始化一次后,后续使用并不会走这个ThreadLocal传递的流程,导致后续提交的任务并不会继承到父线程的线程变量,同时,还会获取到当前任务线程被之前几次任务所修改变量值。
二、TTL介绍
TTL官方github:alibaba/transmittable-thread-local
TransmittableThreadLocal(TTL)
是阿里开源的,用于解决异步执行时上下文传递的问题的组件,在InheritableThreadLocal
基础上,实现了线程复用场景下的线程变量传递功能。
主要使用方式:
1.直接使用
同ThreadLocal:父线程使用TransmittableThreadLocal保存变量,子线程get取出。
2.提交线程池使用
-
增强Runnable或Callable
- 使用TtlRunnable.get()或TtlCallable.get()
- 提交线程池之后,在run()内取出变量
-
增强线程池
- 使用
TtlExecutors.getTtlExecutor()
或getTtlExecutorService()、getTtlScheduledExecutorService()
获取装饰后的线程池 - 使用线程池提交普通任务
- 在
run()
方法内取出变量(任务子线程)
- 使用
装饰线程池其实本质也是装饰Runnable,只是将这个逻辑移到了ExecutorServiceTtlWrapper.submit()
方法内,对所有提交的Runnable都进行包装:
3.对Jdk自带ThreadLocal支持
在2.11.0版本后,还增加了对原生ThreadLocal的支持,主要是针对用户依赖的库中使用ThreadLocal,又无法修改其代码的情况。
- 使用Jdk自带ThreadLocal
- 调用
TransmittableThreadLocal.Transmitter.registerThreadLocal()
将ThreadLocal内的变量值缓存 - 构建
TtlRunnable
,提交到线程池 - 在
run()
方法内取出变量(任务子线程)
- 调用
三、核心原理分析
根据TransmittableThreadLocal的使用流程,其核心逻辑可以分成三个部分:设置线程变量 -> 构建TtlRunnable -> 提交线程池运行
1.设置线程变量
当调用TransmittableThreadLocal.set()
设置变量值时,除了会通过调用super.set()
(ThreadLocal)设置当前线程变量外,还会执行addThisToHolder()
方法:
-
TransmittableThreadLocal内部维护了一个静态的线程变量holder,保存的是以TransmittableThreadLocal对象为Key的Map(这个map的值永远是null,也就是当做Set使用的)
- holder保存了当前线程下的所有TTL线程变量
-
设值时向获取holder传入this,保存发起set()操作的TransmittableThreadLocal对象
2.构建TtlRunnable对象
构建TtlRunnable对象时,会保存原Runnable对象引用,用于后续run()方法中业务代码的执行。另外还会调用TransmittableThreadLocal.Transmitter.capture()
方法,缓存当前主线程的线程变量:
- 这里实际上就是对第一步在holder中保存的ThreadLocal对象进行遍历,保存其变量值
- 此时原本通过ThreadLocal保存的和Thread绑定的线程变量,就复制了一份到TtlRunnable对象中了
3.在子线程中读取变量
当TtlRunnable对象被提交到线程池执行时,调用TtlRunnable.run()
:
注意此时已处于任务子线程环境中
这里会从Runnable对象取出缓存的线程变量captured,然后进行后续流程:
(1)前序处理
TransmittableThreadLocal.Transmitter.replay()
:
- 将缓存的父线程变量值设置到当前任务线程(子线程)的ThreadLocal内,并将父线程的线程变量备份
(2)执行run()方法,读取变量值
由于上一步已经将从父线程复制的线程变量都设置到当前子线程的ThreadLocal中,因此run()方法中直接通过ThreadLocal.get()即可读取继承自父线程的变量值。
(3)后续处理
TransmittableThreadLocal.Transmitter.restore()
:
- 将run()执行前获取的备份,设置到当前线程中去,恢复run()执行过程中可能导致的变化,避免对后续复用此线程的任务产生影响
整个流程可参考官方给出的时序图帮助理解:
四、总结
首先,从使用上来看,不管是修饰Runnable还是修饰线程池,本质都是将Runnable增强为TtlRunnable。
而从实现线程变量传递的原理上来看,TTL做的实际上就是将原本与Thread绑定的线程变量,缓存一份到TtlRunnable对象中,在执行子线程任务前,将对象中缓存的变量值设置到子线程的ThreadLocal中以供run()方法的代码使用,然后执行完后,又恢复现场,保证不会对复用线程产生影响。