TransmittableThreadLocal resuelve el problema de las variables locales del grupo de subprocesos, resulta que siempre lo he entendido mal❌

prefacio


Desde que el autor del marco TransmittableThreadLocal me comentó la última vez, he vuelto a leer el código fuente.Finalmente, este domingo, descubrí que TransmittableThreadLocal resuelve el problema de perder las variables del grupo de subprocesos y descubrí que había un problema con mi entendimiento anterior.

Solía ​​pensar que InheritableThreadLocal resuelve el problema de la transmisión de variables entre subprocesos principales y secundarios. No hay nada de malo en esto, principalmente porque TransmittableThreadLocal resuelve el problema de perder las variables del grupo de subprocesos . Siempre pensé que no podía obtener las variables locales de el subproceso principal, pero el resultado fue abofeteado, porque el grupo de subprocesos El primer lote de subprocesos secundarios son creados por el subproceso principal y pertenecen a los subprocesos principales y secundarios .

El problema más crítico es que el grupo de subprocesos reutilizará el subproceso anterior, de modo que después de actualizar la variable local del subproceso principal, el subproceso creado anteriormente no puede obtener este valor.

Así que vamos a ver cómo se resuelve ~

InheritableThreadDefecto local

¿Puede el primer lote de subprocesos en el grupo de subprocesos obtener la variable de subproceso principal?

Probémoslo a través de una demostración.


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

La impresión es 1. El método de obtención de ThreadLocal utiliza el mapa en el subproceso actual para encontrar el valor.Dado que el valor del subproceso principal se puede encontrar en el subproceso secundario, significa que los subprocesos secundarios creados por el primer lote de grupos de subprocesos se copiará a las variables del subproceso principal. Es el crédito de InheritableThreadLocal

image.png

Entonces, ¿dónde está la falla en InheritableThreadLocal?

Su defecto es en realidad lo que TransmittableThreadLocal tiene que resolver. El principal problema es la reutilización de subprocesos del grupo de subprocesos.Todo el mundo ha oído hablar de la tecnología de agrupación, pero usted nunca ha oído hablar de ella. Es solo que la conexión está activa y no tiene que obtener una nueva cada vez.

Significa que si cambio el subproceso principal más tarde, el subproceso secundario no actualizará su mapa de variables locales y surge el problema clave ~

Veamos el 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;


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

Supongo que te gusta

Origin juejin.im/post/7118772191251922952
Recomendado
Clasificación