TransmittableThreadLocal 线程池场景下复用线程取数据原理浅析

400 阅读5分钟

一句话表述:在创建任务时记录主线程数据快照,执行任务时对当前线程重放快照,实现任务级别的数据隔离

项目名:transmittable-thread-local
GIT 地址:github.com/alibaba/tra…
maven 依赖配置:

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>transmittable-thread-local</artifactId>
    <version>2.12.1</version>
</dependency>

架构设计

用到的一些类

TransmittableThreadLocal
TransmittableThreadLocal.Transmitter:发射机,包含数据处理行为
TransmittableThreadLocal.Transmitter.Snapshot:快照实体,没有行为
TtlCopier:有复制行为的接口
TtlExecutors 线程池执行器
ExecutorServiceTtlWrapper 线程池
TtlCallable 异步可执行任务
TtlRunnable 同步可执行任务

实现原理

  • ThreadLocal 的数据都是存在 Thread#threadLocals 中,做了线程隔离;

  • InheritableThreadLocal 在创建线程时复制主线程数据;

  • transmittable-thread-local 在创建任务时记录主线程数据快照,执行任务时重放快照,实现任务级别的数据隔离。

以下面的 demo 为例,执行过程分为几个步骤:创建线程池,创建任务,提交任务,执行任务

public void demo() {
  ThreadLocal<String> a = new TransmittableThreadLocal<>();
  a.set("1");
  //线程池实现类 ExecutorServiceTtlWrapper
  ExecutorService pool = TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(1));
  //任务类 TtlRunnable
  pool.submit(() -> {System.out.println(a.get());});//a=1
  a.set("2");
  pool.submit(() -> {System.out.println(a.get());});//a=2
}

创建任务

源码位置:com.alibaba.ttl.TtlRunnable#TtlRunnable
关键操作:TransmittableThreadLocal.Transmitte#capture(),生成主线程数据快照对象(Snapshot),记录在任务中( TtlRunnable.capturedRef)

执行任务

源码位置:com.alibaba.ttl.TtlRunnable#run
关键操作:
1、TransmittableThreadLocal.Transmitte#replay() 备份当前线程数据(生成 backup),重放快照(根据之前的快照 capturedRef);
2、执行任务;
3、TransmittableThreadLocal.Transmitte#restore() 恢复当前线程数据(根据之前的备份 backup)。

#restore 目的是什么?

提交到线程池的任务可能在本线程直接执行,#restore 确保没有上面的 Bug

TransmittableThreadLocal.holder 是干嘛的?

用于存/取当前线程的 TransmittableThreadLocal 数据。TransmittableThreadLocal 实际还是存储在 Thread#inheritableThreadLocals 中,以一个 WeakHashMap<TransmittableThreadLocal<Object>, ?> 对象的形式。

测试用例-复杂场景

测试不同时场景下变量的值的变化:
变量a:主线程-创建任务前[新增];主线程-创建任务后[修改];子线程-任务执行中[修改];
变量b:主线程-创建任务后[新增];子线程-任务执行中[修改];
变量c:主线程-创建任务前[新增];子线程-任务执行中[移除];
变量d:子线程-任务执行中[新增];
变量e:主线程-创建任务前[新增];主线程-执行任务后[修改];

测试用例中注释了不同时刻下变量的值,可以根据执行流程来看:1) 创建任务 2) 线程池创建工作线程 3) 线程池工作线程执行任务。也可以关注某个变量在不同时刻的变化,比如变量a,一边 debug 源码一边观察。

总结:
创建任务时录制主线程数据快照(capturedRef)
执行任务时先备份当前线程数据(backup),再对当前线程重放任务快照(capturedRef)
【备份操作的数据】:创建任务后新增的变量会被备份(b),创建任务后修改的变量会被备份(a)
【重放操作的数据】创建任务后新增的变量会被移除(b),创建任务后修改的变量会被恢复值(a) 执行完恢复线程数据(backup) 【恢复操作的数据】任务执行中新增的变量会被移除(d),任务执行中移除的变量会被恢复(c),创建任务后新增的变量会被新增(b),任务执行中修改的变量会被恢复(a)

    @Test
    public void testTransmittableThreadLocal() {
        ThreadLocal<String> a = new TransmittableThreadLocal<>();
        a.set("a=1 主线程-创建任务前的变量a: 初始值1");//主线程 inheritableThreadLocals: <a,1>
        ThreadLocal<String> c = new TransmittableThreadLocal<>();
        c.set("c=1 主线程-创建任务前的变量c: 初始值1");//主线程 inheritableThreadLocals: <a,1>, <c,1>
        ThreadLocal<String> e = new TransmittableThreadLocal<>();
        e.set("e=1 主线程-创建任务前的变量e: 初始值1");//主线程 inheritableThreadLocals: <a,1>, <c,1>, <e,1>
        ExecutorService pool = TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(1));
        /*
        1) 创建任务时主线程值a=1,c=1,e=1; 任务快照值a=1,c=1,e=1
        - 主线程 TransmittableThreadLocal.holder: <a,1>, <c,1>, <e,1>
        - [+]任务快照 TtlRunnable.capturedRef: <a,1>, <c,1>, <e,1>
         */
        TtlRunnable A = TtlRunnable.get(() -> {
            System.out.println(MessageFormat.format("{0} \nb= \n{1} \n{2}", a.get(), c.get(), e.get()));
            a.set("a=3 子线程-创建任务前的变量a: 执行任务中修改为3");
            c.remove();
            ThreadLocal<String> d = new TransmittableThreadLocal<>();
            d.set("d=1 主线程-执行任务中的变量d: 初始值1");
            System.out.println("finish call");
        });
        a.set("a=2 主线程-创建任务前的变量a: 创建任务后修改为2");//主线程 inheritableThreadLocals: <a,2>, <c,1>, <e,1>
        ThreadLocal<String> b = new TransmittableThreadLocal<>();
        b.set("b=1 主线程-创建任务后的变量b: 初始值1");//主线程 inheritableThreadLocals: <a,2>, <b,1>, <c,1>, <e,1>

        /*
        2) 线程池创建工作线程时主线程值a=2,b=1,c=1,e=1; 子线程值a=2,b=1,c=1,e=1
        - 主线程 TransmittableThreadLocal.holder: <a,2>, <b,1>, <c,1>, <e,1>
        - [+]子线程 TransmittableThreadLocal.holder: <a,2>, <b,1>, <c,1>, <e,1>

        3) 执行任务前主线程值a=2,b=1,c=1,e=1; 子线程值a=2,b=1,c=1,e=1; 任务快照值a=1,c=1,e=1
        - 主线程 TransmittableThreadLocal.holder: <a,2>, <b,1>, <c,1>, <e,1>
        - 子线程 TransmittableThreadLocal.holder: <a,2>, <b,1>, <c,1>, <e,1>
        - 任务快照 TtlRunnable.capturedRef: <a,1>, <c,1>, <e,1>

        #replay 执行任务前对子线程的值进行重放(依照任务快照):修改a=1,移除b
        - 主线程 TransmittableThreadLocal.holder: <a,2>, <b,1>, <c,1>, <e,1>
        - [m]子线程 TransmittableThreadLocal.holder: <a,1>, <c,1>, <e,1>
        - 任务快照 TtlRunnable.capturedRef: <a,1>, <c,1>, <e,1>
        - backup 子线程 TransmittableThreadLocal.holder: <a,2>, <b,1>, <c,1>, <e,1>

        > 注意这里同样是变量a,在不同线程中执行结果不同
        在主线程中执行 a#get(),是从主线程 Thread.inheritableThreadLocals 取结果,得到 1
        在子线程中执行 a#get(),是从子线程 Thread.inheritableThreadLocals 取结果,得到 2

        #call 执行任务:修改a=3,移除c,新增d
        - 主线程 TransmittableThreadLocal.holder: <a,2>, <b,1>, <c,1>, <e,1>
        - [m]子线程 TransmittableThreadLocal.holder: <a,3>, <d,1>, <e,1>

        > 这里操作的都是子线程的 Thread.inheritableThreadLocals
        原因在上面 #replay 中讲了

        #restore 执行任务后对子线程的值进行恢复(依照backup):修改a=2,新增b=1,新增c=1,移除d
        - 主线程 TransmittableThreadLocal.holder: <a,2>, <b,1>, <c,1>, <e,1>
        - [m]子线程 TransmittableThreadLocal.holder: <a,2>, <b,1>, <c,1>, <e,1>
        - 任务快照 TtlRunnable.capturedRef: <a,1>, <c,1>, <e,1>
        - backup 子线程 TransmittableThreadLocal.holder: <a,2>, <b,1>, <c,1>, <e,1>

        > debug明明执行了remove d,为什么子线程holder数量还是4???
        可能是 idea watch 值的关系
         */
        pool.submit(A);

        e.set("e=2 主线程-创建任务前的变量e: 修改值为2");//主线程 inheritableThreadLocals: <a,1>, <c,1>, <e,2>

        /*
        4) 执行任务前主线程值a=2,b=1,c=1,e=2; 子线程值a=2,b=1,c=1,e=1; 任务快照值a=1,c=1,e=1
        - 主线程 TransmittableThreadLocal.holder: <a,2>, <b,1>, <c,1>, <e,2>
        - 子线程 TransmittableThreadLocal.holder: <a,2>, <b,1>, <c,1>, <e,1>
        - 任务快照 TtlRunnable.capturedRef: <a,1>, <c,1>, <e,1>

        #replay 执行任务前对子线程的值进行重放(依照任务快照):修改a=1,移除b
        - 主线程 TransmittableThreadLocal.holder: <a,2>, <b,1>, <c,1>, <e,2>
        - [m]子线程 TransmittableThreadLocal.holder: <a,1>, <c,1>, <e,1>
        - 任务快照 TtlRunnable.capturedRef: <a,1>, <c,1>, <e,1>
        - backup 子线程 TransmittableThreadLocal.holder: <a,2>, <b,1>, <c,1>, <e,1>

        #call 执行任务:修改a=3,移除c,新增d
        - 主线程 TransmittableThreadLocal.holder: <a,2>, <b,1>, <c,1>, <e,1>
        - [m]子线程 TransmittableThreadLocal.holder: <a,3>, <d,1>, <e,1>
         */
        pool.submit(A);
    }