TransmittableThreadLocal解决线程池本地变量问题,原来我一直理解错了❌

·  阅读 21074
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的功劳

image.png

那么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了,那么本地线程变量也能读到了。

image.png

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

image.png

这是一个封装类,把ExecutorService包进去,那它关键做了什么

image.png

image.png

好家伙,把Runnable,callable封装了一层,然后再给线程池提交

TtlRunnable

image.png

image.png

看到了吗?最核心的来了,快照,还有Transmitter发射器

Transmitter

image.png

这个发射器里头有快照,快照保存什么呢?

我们可以想象成两个值【所有父子线程变量,子线程自身变量】

我们注意下hold这个类,是一个全局静态变量,类似一个收集者。

它的思路是怎样的呢?

image.png

我们再根据demo进行debug进去看看

com.alibaba.ttl.TtlRunnable#run

image.png

分为三部分,分别是取出旧的快照,然后把新快照塞进子线程,然后再把旧快照补回去子线程。

  1. 取出旧的快照
Object captured = capturedRef.get();
复制代码
  1. 把新快照塞进子线程

image.png

原文叫重放

image.png

首先它拿到所有的变量,塞到backup里头,然后做了一次更新操作,比如说我一个子线程删除了,是不是要把hold这个统计里头剔除掉对吧

setTtlValuesTo

这个就是最重要的把父变量塞到子线程里头

image.png

  1. 把旧快照backup塞回子线程

为啥?因为线程复用,比如说A线程塞了一个xx,下次其实应该拿不到了,但是实际上因为线程复用导致还能拿到,所以我们需要将旧快照塞回去。

image.png

总结

TransmittableThreadLocal通过将线程封装成TtlRunnable,然后通过快照还有hold一个总收集变量东西来解决

agent无侵入实现

image.png

其实就是改写excute方法,塞入改造后的TtlRunnable,而不是之前的Runnable;


我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿

分类:
后端
标签:
收藏成功!
已添加到「」, 点击更改