Transmittable-Thread-Local:线程间上下文传递解决方案
TTL(transmittable-thread-local)是一个线程间传递ThreadLocal,异步执行时上下文传递的解决方案。整个库的核心是构建在TransmittableThreadLocal类(继承并加强InheritableThreadLocal类)之上,同时包含线程池修饰(ExecutorService/ForkJoinPool/TimerTask)以及Java Agent支持,代码小于1k行,短小精悍。
在往下看之前,最好大致看下 https://github.com/alibaba/transmittable-thread-local 文档,效果会更好。JDK的InheritableThreadLocal类可以完成父线程到子线程的值传递。但对于使用线程池等会池化复用线程的组件的情况,线程由线程池创建好,并且线程是池化起来反复使用的;这时父子线程关系的ThreadLocal值传递已经没有意义,应用需要的实际上是把 任务提交给线程池时的ThreadLocal值传递到 任务执行时。原理是使用TtlRunnable/Ttlcallable包装了Runnable/Callable类:
- 在TtlRunnable/Ttlcallable初始化时capture TransmittableThreadLocal变量
- 在run方法调用runnable.run()前进行replay,设置到当前线程ThreadLocal
- 在run方法调用runnable.run()后进行restore,上下文还原,也就是replay的反向操作
注意,步骤1和步骤2/3不是在同一个线程中执行的。
既然TTL的TransmittableThreadLocal是继承并加强InheritableThreadLocal类的,那么首先需要分析下InheritableThreadLocal是什么东东,源码如下:
1 | public class InheritableThreadLocal<T> extends ThreadLocal<T> { |
Thread类中有两个ThreadLocal相关的ThreadLocalMap属性,如下:
1 | ThreadLocal.ThreadLocalMap threadLocals:ThreadLocal变量使用 |
新建线程时,将当前线程的inheritableThreadLocals传递给新线程,这里的传递是对InheritableThreadLocal变量的数据做浅拷贝(引用复制),这样新线程可以使用同一个InheritableThreadLocal变量查看上一个线程的数据。
下面分析下使用InheritableThreadLocal的一个demo:
1 | void testInheritableThreadLocal_线程池() throws InterruptedException { |
上面代码在submit任务时会伴随着(线程池工作)线程的创建,会继承当前线程的InheritableThreadLocal,所以会有上述输出结果。如果将代码改成下面的样子,会有什么不同呢?
1 | void testInheritableThreadLocal_线程池() throws InterruptedException { |
因为创建线程时当前线程并没有inheritableThreadLocals,所以线程池中线程打印结果为null。这种场景下如何才能获取到parent变量的数据呢?这时就该TTL出场了,将上述代码改成TTL方式如下:
1 | void testTtlInheritableThreadLocal_线程池() throws InterruptedException { |
下面以TtlRunnable.get()为起点分析TTL的设计实现,TtlRunnable.get源码如下(TtlRunnable.get流程对应的初始化时capture操作,保存快照。TtlCallable和TtlRunnable流程类似):
1 | public static TtlRunnable get(@Nullable Runnable runnable) { |
在新建TtlRunnable过程中,会保存下TransmittableThreadLocal.holder到captured,记录到TtlRunnable实例中的capturedRef字段,TransmittableThreadLocal.holder类型是:
1 | // Note about holder: |
从上面代码我们知道初始化TtlRunnable时已经将TransmittableThreadLocal保存下来了,那么什么时候应用到当前线程ThreadLocal中呢,这是就需要看下TtlRunnable.run方法:
1 | public void run() { |
注意,TTL中的replay操作是以captured为当前inheritableThreadLocals的(处理逻辑是在TtlRunable run时,会以TtlRunnable.get时间点获取的captured(类似TTL快照)为准,holder中不在captured的先移除,在的会被替换)。关于replay中的clean extra TTL value讨论可以参考:https://github.com/alibaba/transmittable-thread-local/issues/134。回放captured和执行完runnable.run之后,再restore恢复到原来inheritableThreadLocals的状态。
说完了TTL中的capture、replay和restore流程,再看下官方提供的这个时序图,是不是感觉清晰很多。
除了通过TtlRunable.get()修饰用户自定义的task之外,还可以修饰线程池和使用Java Agent修饰JDK线程池实现类的方式实现TTL功能。
修饰线程池省去每次Runnable和Callable传入线程池时的修饰,这个逻辑可以在线程池中完成,其实就是在提交task时调用TtlRunable.get()修饰下。通过工具类com.alibaba.ttl.threadpool.TtlExecutors完成,有下面的方法:
- getTtlExecutor:修饰接口Executor
- getTtlExecutorService:修饰接口ExecutorService
- getTtlScheduledExecutorService:修饰接口ScheduledExecutorService
使用Java Agent来修饰JDK线程池实现类,这种方式,实现线程池的传递是透明的,代码中没有修饰Runnable或是线程池的代码。即可以做到应用代码无侵入。关于 无侵入 的更多说明参见文档 Java Agent方式对应用代码无侵入。
使用Java Agent 实例如下:
1 | // 启动参数中需加上 -javaagent:path/transmittable-thread-local-x.x.x.jar |
TTL代码实现来看,确实短小精悍,值得花几个小时看下源码。通过看源码,我发现了,可以通过new ThreadLocal对象时,直接重写其initialValue方法,可以在threadLocal.get为空时初始化一个值,使用示例如下:
1 | ThreadLocal<String> local = new ThreadLocal<String>() { |
- 本文链接:http://luoxn28.github.io/2020/03/16/transmittable-thread-local-xian-cheng-jian-shang-xia-wen-chuan-di-jie-jue-fang-an/
- 版权声明:本文章著作权归作者所有,任何形式的转载都请注明出处。
分享