TransmittableThreadLocal resolve o problema das variáveis locais do pool de threads, acontece que eu sempre entendi errado❌

prefácio


Como o autor do framework TransmittableThreadLocal comentou sobre mim da última vez, reli o código-fonte. Finalmente, neste domingo, descobri que TransmittableThreadLocal resolve o problema de perder variáveis ​​do pool de threads e descobri que havia um problema com meu entendimento anterior.

Eu costumava pensar que InheritableThreadLocal resolve o problema de transmissão de variáveis ​​entre threads pai e filho. Não há nada de errado com isso, principalmente porque TransmittableThreadLocal resolve o problema de perder variáveis ​​do pool de threads . Eu sempre pensei que não poderia obter as variáveis ​​locais de o encadeamento pai, mas o resultado foi batido, porque o conjunto de encadeamentos O primeiro lote de encadeamentos filho é criado pelo encadeamento principal e pertence aos encadeamentos pai e filho .

O problema mais crítico é que o conjunto de encadeamentos reutilizará o encadeamento anterior, de modo que, após a atualização da variável local do encadeamento pai, o encadeamento filho criado anteriormente não possa obter esse valor.

Então vamos ver como isso é resolvido~

Falha InheritableThreadLocal

O primeiro lote de threads no pool de threads pode obter a variável de thread pai?

Vamos experimentá-lo através de uma demonstração


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());
});

A impressão é 1. O método get ThreadLocal usa o mapa no thread atual para encontrar o valor. Como o valor do thread pai pode ser encontrado no thread filho, isso significa que os threads filhos criados pelo primeiro lote de pools de threads será copiado para as variáveis ​​do thread pai. É o crédito de InheritableThreadLocal

image.png

Então, onde está a falha em InheritableThreadLocal?

Seu defeito é realmente o que TransmittableThreadLocal tem que resolver. O principal problema é a reutilização de threads do pool de threads. Todo mundo já ouviu falar da tecnologia de pooling, mas você nunca ouviu falar dela. É só que a conexão está quente e você não precisa obter uma nova toda vez.

Isso significa que, se eu alterar o thread pai mais tarde, o thread filho não atualizará seu mapa de variável local e o problema principal surgirá ~

Vamos ver o código

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;


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

Guess you like

Origin juejin.im/post/7118772191251922952