小结实现思路
- 新建一个 YesThreadLocal 类继承自 ThreadLocal ,用于标识这个修饰的变量需要父子线程拷贝
- 新建一个 YesRunnable 类继承自 Runnable,采用装饰器模式,这样就不用修改原有的 Runnable。在构造阶段复制父线程的 YesThreadLocal 变量赋值给 YesRunnable 的一个成员变量 threadlocalCopy 保存。修饰 run 方法,在真正逻辑执行前将 threadlocalCopy 赋值给当前执行线程的上下文,且保存当前线程之前的上下文,在执行完毕之后,再复原此线程的上下文
- 由于需要在构造的时候复制所有父线程用到的 YesThreadLocal ,因此需要有个 holder 变量来保存所有用到的 YesThreadLocal ,这样在构造的时候才好遍历赋值。并且 holder 变量也需要线程隔离,所以用 ThreadLocal 修饰,并且为了防止 holder 强引用导致内存泄漏,所以用 WeakHashMap 存储。
- 往 holder 添加 YesThreadLocal 的时机就在 YesThreadLocal#set 之时
TransmittableThreadLocal 的实现
这篇只讲 TTL 核心思想(关键路径),由于篇幅原因其它的不作展开,之后再写一篇详细的。
我上面的实现其实就是 TTL 的复制版,如果你理解了上面的实现,那么接下来对 TTL 介绍理解起来应该很简单,相当于复习了。
我们先简单看一下 TTL 的使用方式。
使用起来很简单对吧?
TTL 对标上面的 YesThreadLocal ,差别在于它继承的是 InheritableThreadLocal,因为这样直接 new TTL 也会拥有父子线程本地变量的传递能力。
所以,在父线程赋值即执行 set 操作之后,父线程里的 holder 就存储了当前的 TTL 对象了,即上面演示代码的 ttl.set() 操作。
然后重点就移到了TtlRunnable.get
上了,根据上面的理解我们知道这里是要进行一个装饰的操作,这个 get 代码也比较简单,核心就是 new 一个 TtlRunnable 包装了原始的 task。
这个 capturedRef 其实就是父线程本地变量的拷贝,然后 capture()
其实就等同于copyFatherThreadlocal()
再来看一下 TtlRunnable 装饰的 run 方法:
逻辑很清晰的四步骤:
- 拿到父类本地变量拷贝
- 赋值给当前线程(线程池内的某线程),并保存之前的本地变量
- 执行逻辑
- 复原当前线程之前的本地变量
我们再来分析一下 capture()
方法,即如何拷贝的。
在 TTL 中是专门定义了一个静态工具类 Transmitter 来实现上面的 capture、 replay、restore 操作。
可以看到 capture 的逻辑其实就是返回一个快照,而这个快照就是遍历 holder 获取所有存储在 holder 里面的 TTL ,返回一个新的 map,还是很简单的吧!
这里还有个 captureThreadLocalValues ,这个是为兼容那些无法将 ThreadLocal 类变更至 TTL ,但是又想复制传递 ThreadLocal 的值而使用的,可以先忽略。
我们再来看看 replay,即如何将父类的本地变量赋值给当前线程的。
就是 for 循环进行了一波 set ,从这里也可以得知为什么上面需要移除父线程没有的 TTL,因为这里只是进行了 set。如果不 remove 当前线程的本地变量,那就不是完全继承自父线程的本地变量了,可能掺杂着之前的本地变量,也就是不干净了,防止这种干扰,所以还是 remove 了为妙。
最后我们看下 restore 操作:
至此想必对 TTL 的原理应该都很清晰了吧!
一些用法
上面我们展示的只是其中一个用法也就是利用 TtlRunnable.get
来包装 Runnable。
TTL 还提供了线程池的修饰方法,即 TtlExecutors,比如可以这样使用:
ExecutorService executorService = TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(1));
其实原理也很简单,装饰了一下线程池提交任务的方法,里面实现了 TtlRunnable.get
的包装
还有一种使用方式更加透明,即利用 Java Agent 来修饰 JDK 的线程池实现类,这种方式在使用上基本就是无感知了。
在 Java 的启动参数加上:-javaagent:path/to/transmittable-thread-local-2.x.y.jar 即可,然后就正常的使用就行,原生的线程池实现类已经悄悄的被改了!
TransmittableThreadLocal<String> ttl= new TransmittableThreadLocal<>(); ExecutorService executorService = Executors.newFixedThreadPool(1); Runnable task = new RunnableTask(); executorService.submit(task);
最后
好了,有关 TTL 的原理和用法解释的都差不多了。
总结下来的核心操作就是 CRR(Capture/Replay/Restore),拷贝快照、重放快照、复原上下文。
可能有些人会疑惑为什么需要复原,线程池的线程每次执行的时候,如果用了 TTL 那执行的线程都会被覆盖上下文,没必要复原对吧?
其实也有人向作者提了这个疑问,回答是:
- 线程池满了且线程池拒绝策略使用的是『CallerRunsPolicy』,这样执行的线程就变成当前线程了,那肯定是要复原的,不然上下文就没了。
- 使用ForkJoinPool(包含并行执行Stream与CompletableFuture,底层使用ForkJoinPool)的场景,展开的ForkJoinTask会在调用线程中直接执行。
其实关于 TTL 还有很多细节可以说,不过篇幅有限,细节要说的话得再开一章。不过今天这篇也算把 TTL 的核心思想讲完了。
假设现在有个面试官问你,我要向线程池里面传递 ThreadLocal 怎么实现呀?想必你肯定可以回答出来了~
好了,后面有时间我再出一篇 TTL 的细节~等我哈。
欢迎关注我的公众号【yes的练级攻略】,更多文章,等你来阅!