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
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了,那么本地线程变量也能读到了。
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;
我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿。