ThreadLocal系列(三)-TransmittableThreadLocal的使用及原理解析
一、基本使用
首先,TTL
是用来解决ITL
解决不了的问题而诞生的,所以TTL
一定是支持父线程的本地变量传递给子线程这种基本操作的,ITL
也可以做到,但是前面有讲过,ITL
在线程池
的模式下,就没办法再正确传递了,所以TTL
做出的改进就是即便是在线程池模式下,也可以很好的将父线程本地变量传递下去,先来看个例子:
1 | // 需要注意的是,使用TTL的时候,要想传递的值不出问题,线程池必须得用TTL加一层代理(下面会讲这样做的目的) |
运行结果:
1 | 线程名称-Thread-2, 变量值=4 |
程序有些啰嗦,为了说明问题,加了很多说明,但至少通过上面的例子,不难发现,两个主线程里都使用线程池异步,而且值在主线程里还发生过改变,测试结果展示一切正常,由此可以知道TTL
在使用线程池
的情况下,也可以很好的完成传递,而且不会发生错乱。
那么是不是对普通线程异步也有这么好的支撑呢?
改造下上面的测试代码:
1 | private static ThreadLocal tl = new TransmittableThreadLocal<>(); |
相比代码块1
,这一段的异步全都是普通异步,未采用线程池的方式进行异步,看下运行结果:
1 | 本地变量改变之后(4), 父线程名称-main_02, 子线程名称-Thread-14, 变量值=4 |
ok,可以看到,达到了跟第一个测试一致的结果。
到这里,通过上述两个例子,TTL
的基本使用,以及其解决的问题,我们已经有了初步的了解,下面我们来解析一下其内部原理,看看TTL
是怎么完成对ITL
的优化的。
二、原理分析
先来看TTL
里面的几个重要属性及方法
TTL
定义:
1 | public class TransmittableThreadLocal extends InheritableThreadLocal |
可以看到,TTL
继承了ITL
,意味着TTL
首先具备ITL
的功能。
再来看看一个重要属性holder
:
1 | /** |
再来看下set
和get
:
1 | //下面的方法均属于TTL类 |
TTL
里先了解上述的几个方法及对象,可以看出,单纯的使用TTL
是达不到支持线程池
本地变量的传递的,通过第一部分的例子,可以发现,除了要启用TTL
,还需要通过TtlExecutors.getTtlExecutorService
包装一下线程池才可以,那么,下面就来看看在程序即将通过线程池异步的时候,TTL
帮我们做了哪些操作(这一部分是TTL
支持线程池传递的核心部分):
首先打开包装类,看下execute
方法在执行时做了些什么。
1 | // 此方法属于线程池包装类ExecutorTtlWrapper |
结合上述代码,大致知道了在线程池异步之前需要做的事情,其实就是把当前父线程里的本地变量取出来,然后赋值给Rannable
包装类里的capturedRef
属性,到此为止,下面会发生什么,我们大致上可以猜出来了,接下来大概率会在run
方法里,将这些捕获到的值赋给子线程的holder
赋对应的TTL值
,那么我们继续往下看Rannable
包装类里的run
方法是怎么实现的:
1 | //run方法属于Rannable的包装类TtlRunnable |
根据上述代码,我们看到了TTL
在异步任务执行前,会先进行赋值操作(就是拿着异步发生时捕获到的父线程的本地变量,赋给自己),当任务执行完,就恢复原生的自己本身的线程变量值。
下面来具体看这俩方法:
1 | //下面的方法均属于TTL的静态内部类Transmittable |
ok,到这里基本上把TTL
比较核心的代码看完了,下面整理下整个流程,这是官方给出的时序图:
上图第一行指的是类名称,下面的流程指的是类所做的事情,根据上面罗列出来的源码,结合这个时序图,可以比较直观一些的理解整个流程。
三、TTL中线程池子线程原生变量的产生
这一节是为了验证上面replay
和restore
,现在通过一个例子来验证下,先把源码down下来,在源码的restore
和replay
上分别加上输出语句,遍历holder
:
1 | //replay前后打印holder里面的值 |
代码这样做的目的,就是要说明线程池所谓的原生本地变量是怎么产生的,以及replay和restore是怎么设置和恢复的,下面来看个简单的例子:
1 | private static ExecutorService executorService = TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(1)); |
运行结果如下:
1 | --------------------replay前置,当前拿到的holder里的TTL列表 |
我们会发现,原生值产生了,从异步开始,就确定了线程池里的线程具备了1和2的值,那么,再来改动下上面的测试代码:
1 | public static void main(String[] args) throws InterruptedException { |
运行结果为:
1 | --------------------replay前置,当前拿到的holder里的TTL列表 |
可以发现,第一次异步时,只有一个值被传递了下去,然后第二次异步,新加了一个tl2的值,但是看第二次异步的打印,会发现,restore
恢复后,仍然是第一次异步发生时放进去的那个tl的值。
通过上面的例子,基本可以确认,所谓线程池内线程的本地原生变量,其实是第一次使用线程时被传递进去的值,我们之前有说过TTL
是继承至ITL
的,之前的文章也说过,线程池第一次启用时是会触发Thread
的init
方法的,也就是说,在第一次异步时拿到的主线程的变量会被传递给子线程,作为子线程的原生本地变量保存起来,后续是replay
操作和restore
操作也是围绕着这个原生变量(即原生holder
里的值)来进行设置
、恢复
的,设置的是当前父线程捕获到的本地变量,恢复的是子线程原生本地变量。
holder里持有的可以理解就是当前线程内的所有本地变量,当子线程将异步任务执行完毕后,会执行restore进行恢复原生本地变量,具体参照上面的代码和测试代码。
四、总结
到这里基本上确认了TTL
是如何进行线程池传值的,以及被包装的run
方法执行异步任务之前,会使用replay
进行设置父线程里的本地变量给当前子线程,任务执行完毕,会调用restore
恢复该子线程原生的本地变量(目前原生本地变量的产生,就只碰到上述测试代码中的这一种情况,即线程第一次使用时通过ITL
属性以及Thread
的init
方法传给子线程,还不太清楚有没有其他方式设置)。
其实,正常程序里想要完成线程池上下文传递,使用TL
就足够了,我们可以效仿TTL
包装线程池对象
的原理,进行值传递,异步任务结束后,再remove
,以此类推来完成线程池值传递,不过这种方式过于单纯,且要求上下文为只读对象,否则子线程存在写操作,就会发生上下文污染。
TTL
项目地址(可以详细了解下它的其他特性和用法):https://github.com/alibaba/transmittable-thread-local