TransmittableThreadLocal解决线程池本地变量问题,原来我一直理解错了
Posted 大鸡腿同学
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了TransmittableThreadLocal解决线程池本地变量问题,原来我一直理解错了相关的知识,希望对你有一定的参考价值。
个人博客:👉进入博客,关注下博主,感谢~
🌈所有博客均在上面博客首发,其他平台同步更新
🏆大家一起进步,多多指教~
前言
自从上次TransmittableThreadLocal框架作者评论我之后,我重新去看了下源码,终于在这个周天,我才把TransmittableThreadLocal解决线程池变量丢失的问题搞明白,而且发现我之前的认识有问题,久久孩子
我之前是觉得,InheritableThreadLocal解决父子线程变量传递的问题,这个没有毛病,主要是TransmittableThreadLocal解决线程池变量丢失问题,我一直以为是拿不到父线程的本地变量的,结果打脸了,因为线程池第一批子线程是main线程创建出来的,属于父子线程。
最关键的问题是,线程池会复用之前的线程,导致父线程的本地变量更新之后,之前创建的子线程拿不到这个值。
那么我们去看下它是怎么解决的~
InheritableThreadLocal缺陷
线程池第一批线程能否拿到父线程变量?
我们通过一个demo来试下
ThreadPoolExecutor executor = new ThreadPoolExecutor(1,1,1, TimeUnit.MINUTES,new ArrayBlockingQueue<>(1));
ThreadLocal local = new InheritableThreadLocal();
local.set(1);
executor.execute(()->
System.out.println("打印1:"+local.get());
);
打印是1,ThreadLocal get方法拿的是当前线程里面map来找值,既然子线程里头能找到父线程的值,说明第一批线程池创建的子线程是会被复制父线程的变量的,也就是InheritableThreadLocal的功劳
那么InheritableThreadLocal的缺陷在哪里?
它的缺陷其实就是TransmittableThreadLocal要去解决的。主要问题是线程池的线程复用,池化技术大家都听过吧,没有听过过来挨打。就是把连接热乎了,不用每次都去拿新的。
意味着,如果我在后面改了父线程,子线程不会更新它的本地变量map,关键问题浮出水面~
我们看下代码
ThreadPoolExecutor executor = new ThreadPoolExecutor(1,1,1, TimeUnit.MINUTES,new ArrayBlockingQueue<>(1));
ThreadLocal local = new InheritableThreadLocal();
local.set(1);
executor.execute(()->
System.out.println("打印1:"+local.get());
);
local.set(2);
System.out.println("打印2:"+local.get());
executor.execute(()->
System.out.println("打印3:"+local.get());
);
它居然打印的还是1,我的天,就是我们刚刚讲的,父线程更新了,子线程拿到还是旧的值。
这样会引发什么问题呢?
如果我在实现apm全链路追踪的功能,我用本地变量缓存当前访问的traceid,使用线程池的话,那么我们下次请求还是会拿到旧的traceid,那就gg
解决方案是什么?
local.set(2);
System.out.println("打印2:"+local.get());
executor.execute(()->
local.set(2);
System.out.println("打印3:"+local.get());
)
解决方案也很简单,就是在线程里头重新set一遍,为啥这样就能解决呢?
回到ThreadLocal get方法上,它是从本地线程去拿的,如果你重新去set了,那么本地线程变量也能读到了。
TransmittableThreadLocal
它如何解决线程池变量更新问题的呢?
我们来看下一个例子
private static ExecutorService TTLExecutor = TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(5));
//定义另外一个线程池循环执行,模拟业务场景下多Http请求调用的情况
private static ExecutorService loopExecutor = Executors.newFixedThreadPool(5);
private static AtomicInteger i=new AtomicInteger(0);
//TTL的ThreadLocal
private static ThreadLocal tl = new TransmittableThreadLocal<>(); //这里采用TTL的实现
public static void main(String[] args)
while (true)
loopExecutor.execute( () ->
if(i.get()<10)
tl.set(i.getAndAdd(1));
TTLExecutor.execute(() ->
System.out.println(String.format("子线程名称-%s, 变量值=%s", Thread.currentThread().getName(), tl.get()));
);
);
它的打印是正常的,就是父线程累加数字,子线程也能正常读取,关键就这TtlExecutors.getTtlExecutorService。
ExecutorServiceTtlWrapper
这是一个封装类,把ExecutorService包进去,那它关键做了什么?
好家伙,把Runnable,callable封装了一层,然后再给线程池提交
TtlRunnable
看到了吗?最核心的来了,快照,还有Transmitter发射器
Transmitter
这个发射器里头有快照,快照保存什么呢?
我们可以想象成两个值【所有父子线程变量,子线程自身变量】
我们注意下hold这个类,是一个全局静态变量,类似一个收集者。
它的思路是怎样的呢?
我们再根据demo进行debug进去看看
com.alibaba.ttl.TtlRunnable#run
分为三部分,分别是取出旧的快照,然后把新快照塞进子线程,然后再把旧快照补回去子线程。
- 取出旧的快照
Object captured = capturedRef.get();
- 把新快照塞进子线程
原文叫重放
首先它拿到所有的变量,塞到backup里头,然后做了一次更新操作,比如说我一个子线程删除了,是不是要把hold这个统计里头剔除掉对吧
setTtlValuesTo
这个就是最重要的把父变量塞到子线程里头
- 把旧快照backup塞回子线程
为啥?因为线程复用,比如说A线程塞了一个xx,下次其实应该拿不到了,但是实际上因为线程复用导致还能拿到,所以我们需要将旧快照塞回去。
总结
TransmittableThreadLocal通过将线程封装成TtlRunnable,然后通过快照还有hold一个总收集变量东西来解决
agent无侵入实现
其实就是改写excute方法,塞入改造后的TtlRunnable,而不是之前的Runnable;
以上是关于TransmittableThreadLocal解决线程池本地变量问题,原来我一直理解错了的主要内容,如果未能解决你的问题,请参考以下文章
阿里开源TransmittableThreadLocal(TTL)l的使用及原理解析
TransmittableThreadLocal相关组件实用解读,及如何达到线程池中的线程复用,及使用在哪些线程数据传递场景?
每日一博 - ThreadLocal VS InheritableThreadLocal VS TransmittableThreadLocal
TransmittableThreadLocal解决线程池本地变量问题,原来我一直理解错了