transmittable-thread-local:解决线程池之间ThreadLocal本地变量传递的问题

Posted 快乐崇拜234

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了transmittable-thread-local:解决线程池之间ThreadLocal本地变量传递的问题相关的知识,希望对你有一定的参考价值。

欢迎关注本人公众号

线程本地变量相关的博客目录

概述

当InheritableThreadLocal遇到线程池:主线程本地变量修改后,子线程无法读取到新值 一文中介绍了InheritableThreadLocal的问题:主线程变量修改后,子线程无法取到的问题。

阿里开源的transmittable-thread-local解决了这个问题。

transmittable-thread-local介绍

git地址:transmittable-thread-local

需求场景

在ThreadLocal的需求场景即是TTL的潜在需求场景,如果你的业务需要『在使用线程池等会池化复用线程的执行组件情况下传递ThreadLocal』则是TTL目标场景。

下面是几个典型场景例子。

  • 分布式跟踪系统
  • 日志收集记录系统上下文
  • Session级Cache
  • 应用容器或上层框架跨应用代码给下层SDK传递信息

实例

public class TransmittableThreadLocalTest1 
    public static ThreadLocal<Integer> threadLocal = new TransmittableThreadLocal<>();
    public static ExecutorService executorService =
            TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(1));

    public static void main(String[] args) throws InterruptedException 
        System.out.println("主线程开启");
        threadLocal.set(1);
        System.out.println("主线程读取本地变量:" + threadLocal.get());

        executorService.submit(() -> 
            System.out.println("子线程读取本地变量:" + threadLocal.get());
        );

        TimeUnit.SECONDS.sleep(1);

        threadLocal.set(2);
        System.out.println("主线程读取本地变量:" + threadLocal.get());

        executorService.submit(() -> 
            //[读到了主线程修改后的新值]
            System.out.println("子线程读取本地变量:" + threadLocal.get());
            threadLocal.set(3);
            System.out.println("子线程读取本地变量:" + threadLocal.get());
        );

        TimeUnit.SECONDS.sleep(1);
        //依旧读取的是 2
        System.out.println("主线程读取本地变量:" + threadLocal.get());
    

运行结果:

主线程开启
主线程读取本地变量:1
子线程读取本地变量:1
主线程读取本地变量:2
子线程读取本地变量:2
子线程读取本地变量:3
主线程读取本地变量:2

可以看到,父线程修改了本地变量后,新提交到线程池的子线程成功读取到新值。

上面是普通的Java integer类型。如果是对象,会是神马情况呢?下面就用Stu这个对象试一下

public class TransmittableThreadLocalTest2 
    public static ThreadLocal<Stu> threadLocal = new TransmittableThreadLocal<>();
    public static ExecutorService executorService =
            TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(1));

    public static void main(String[] args) throws InterruptedException 
        System.out.println("主线程开启");
        threadLocal.set(new Stu("aa",1));
        System.out.println("主线程读取本地变量:" + threadLocal.get());

        executorService.submit(() -> 
            System.out.println("子线程读取本地变量:" + threadLocal.get());
        );

        TimeUnit.SECONDS.sleep(1);

        threadLocal.get().setAge(2);
        System.out.println("主线程读取本地变量:" + threadLocal.get());

        executorService.submit(() -> 
            //[读到了主线程修改后的新值]
            System.out.println("子线程读取本地变量:" + threadLocal.get());
            threadLocal.get().setAge(3);
            System.out.println("子线程读取本地变量:" + threadLocal.get());
        );

        TimeUnit.SECONDS.sleep(1);
        //读取的是2. 原因是因为这里依旧是值传递,主子线程中引用的实际是同一个对象!!
        //如果想传值,则重写TransmittableThreadLocal的copy即可
        System.out.println("主线程读取本地变量:" + threadLocal.get());
    


结果:

主线程开启
主线程读取本地变量:Stu(name=aa, age=1)
子线程读取本地变量:Stu(name=aa, age=1)
主线程读取本地变量:Stu(name=aa, age=2)
子线程读取本地变量:Stu(name=aa, age=2)
子线程读取本地变量:Stu(name=aa, age=3)
主线程读取本地变量:Stu(name=aa, age=3)

重点看最后一条输出日志,age=3,子线程改了threadlocal的值后,主线程的也改变了。这是因为默认情况下是值传递,这里是Stu对象,值传递实际传递的是引用的拷贝。

如果不想值传递怎么办呢?很简单,跟InheritableThreadLocal一样,重写TransmittableThreadLocalcopy方法即可

public class MyTransmittableThreadLocal<T> extends TransmittableThreadLocal<T> 
    public T copy(T parentValue) 
        String s = JSONObject.toJSONString(parentValue);
        return (T)JSONObject.parseObject(s,parentValue.getClass());
    

此时换成我自己定义的MyTransmittableThreadLocal来试一下:

public class TransmittableThreadLocalTest2 
    public static ThreadLocal<Stu> threadLocal = new MyTransmittableThreadLocal<>();
    public static ExecutorService executorService =
            TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(1));

    public static void main(String[] args) throws InterruptedException 
        System.out.println("主线程开启");
        threadLocal.set(new Stu("aa",1));
        System.out.println("主线程读取本地变量:" + threadLocal.get());

        executorService.submit(() -> 
            System.out.println("子线程读取本地变量:" + threadLocal.get());
        );

        TimeUnit.SECONDS.sleep(1);

        threadLocal.get().setAge(2);
        System.out.println("主线程读取本地变量:" + threadLocal.get());

        executorService.submit(() -> 
            //[读到了主线程修改后的新值]
            System.out.println("子线程读取本地变量:" + threadLocal.get());
            threadLocal.get().setAge(3);
            System.out.println("子线程读取本地变量:" + threadLocal.get());
        );

        TimeUnit.SECONDS.sleep(1);
        //读取的是2. 原因是因为这里依旧是值传递,主子线程中引用的实际是同一个对象!!
        //如果想传值,则重写TransmittableThreadLocal的copy即可
        System.out.println("主线程读取本地变量:" + threadLocal.get());
    

结果:

主线程开启
主线程读取本地变量:Stu(name=aa, age=1)
子线程读取本地变量:Stu(name=aa, age=1)
主线程读取本地变量:Stu(name=aa, age=2)
子线程读取本地变量:Stu(name=aa, age=2)
子线程读取本地变量:Stu(name=aa, age=3)
主线程读取本地变量:Stu(name=aa, age=2)

可以看到:最后一条日志的age=2.

实现原理

本节内容来源于博客 TransmittableThreadLocal的使用及原理解析 . 感谢原博主精彩的分析。

官方时序图:

先来看TTL里面的几个重要属性及方法

TTL定义:

public class TransmittableThreadLocal extends InheritableThreadLocal

可以看到,TTL继承了ITL,意味着TTL首先具备ITL的功能。

再来看看一个重要属性holder:

   /**
     * 这是一个ITL类型的对象,持有一个全局的WeakMap(weakMap的key是弱引用,同TL一样,也是为了解决内存泄漏的问题),里面存放了TTL对象
     * 并且重写了initialValue和childValue方法,尤其是childValue,可以看到在即将异步时父线程的属性是直接作为初始化值赋值给子线程的本地变量对象(TTL)的
     */
    private static InheritableThreadLocal<Map<TransmittableThreadLocal<?>, ?>> holder =
            new InheritableThreadLocal<Map<TransmittableThreadLocal<?>, ?>>() 
                @Override
                protected Map<TransmittableThreadLocal<?>, ?> initialValue() 
                    return new WeakHashMap<TransmittableThreadLocal<?>, Object>();
                

                @Override
                protected Map<TransmittableThreadLocal<?>, ?> childValue(Map<TransmittableThreadLocal<?>, ?> parentValue) 
                    return new WeakHashMap<TransmittableThreadLocal<?>, Object>(parentValue);
                
            ;

再来看下set和get:

//下面的方法均属于TTL类
@Override
    public final void set(T value) 
        super.set(value);
        if (null == value) removeValue();
        else addValue();
    

    @Override
    public final T get() 
        T value = super.get();
        if (null != value) addValue();
        return value;
    
    
    private void removeValue() 
        holder.get().remove(this); //从holder持有的map对象中移除
    

    private void addValue() 
        if (!holder.get().containsKey(this)) 
            holder.get().put(this, null); //从holder持有的map对象中添加
        
    

TTL里先了解上述的几个方法及对象,可以看出,单纯的使用TTL是达不到支持线程池本地变量的传递的,通过第一部分的例子,可以发现,除了要启用TTL,还需要通过TtlExecutors.getTtlExecutorService包装一下线程池才可以,那么,下面就来看看在程序即将通过线程池异步的时候,TTL帮我们做了哪些操作(这一部分是TTL支持线程池传递的核心部分):

首先打开包装类,看下execute方法在执行时做了些什么。

// 此方法属于线程池包装类ExecutorTtlWrapper
@Override
    public void execute(@Nonnull Runnable command) 
        executor.execute(TtlRunnable.get(command)); //这里会把Rannable包装一层,这是关键,有些逻辑处理,需要在run之前执行
    

    // 对应上面的get方法,返回一个TtlRunnable对象,属于TtLRannable包装类
    @Nullable
    public static TtlRunnable get(@Nullable Runnable runnable) 
        return get(runnable, false, false);
    

    // 对应上面的get方法
    @Nullable
    public static TtlRunnable get(@Nullable Runnable runnable, boolean releaseTtlValueReferenceAfterRun, boolean idempotent) 
        if (null == runnable) return null;

        if (runnable instanceof TtlEnhanced)  // 若发现已经是目标类型了(说明已经被包装过了)直接返回
            // avoid redundant decoration, and ensure idempotency
            if (idempotent) return (TtlRunnable) runnable;
            else throw new IllegalStateException("Already TtlRunnable!");
        
        return new TtlRunnable(runnable, releaseTtlValueReferenceAfterRun); //最终初始化
    

    // 对应上面的TtlRunnable方法
    private TtlRunnable(@Nonnull Runnable runnable, boolean releaseTtlValueReferenceAfterRun) 
        this.capturedRef = new AtomicReference<Object>(capture()); //这里将捕获后的父线程本地变量存储在当前对象的capturedRef里
        this.runnable = runnable;
        this.releaseTtlValueReferenceAfterRun = releaseTtlValueReferenceAfterRun;
    

    // 对应上面的capture方法,用于捕获当前线程(父线程)里的本地变量,此方法属于TTL的静态内部类Transmitter
    @Nonnull
    public static Object capture() 
        Map<TransmittableThreadLocal<?>, Object> captured = new HashMap<TransmittableThreadLocal<?>, Object>();
        for (TransmittableThreadLocal<?> threadLocal : holder.get().keySet())  // holder里目前存放的k-v里的key,就是需要传给子线程的TTL对象
            captured.put(threadLocal, threadLocal.copyValue());
        
        return captured; // 这里返回的这个对象,就是当前将要使用线程池异步出来的子线程,所继承的本地变量合集
    

    // 对应上面的copyValue,简单的将TTL对象里的值返回(结合之前的源码可以知道get方法其实就是获取当前线程(父线程)里的值,调用super.get方法)
    private T copyValue() 
        return copy(get());
    
    protected T copy(T parentValue) 
        return parentValue;
    

结合上述代码,大致知道了在线程池异步之前需要做的事情,其实就是把当前父线程里的本地变量取出来,然后赋值给Rannable包装类里的capturedRef属性,到此为止,下面会发生什么,我们大致上可以猜出来了,接下来大概率会在run方法里,将这些捕获到的值赋给子线程的holder赋对应的TTL值,那么我们继续往下看Rannable包装类里的run方法是怎么实现的:

//run方法属于Rannable的包装类TtlRunnable

@Override
    public void run() 
        Object captured = capturedRef.get(); // 获取由之前捕获到的父线程变量集
        if (captured == null || releaseTtlValueReferenceAfterRun && !capturedRef.compareAndSet(captured, null)) 
            throw new IllegalStateException("TTL value reference is released after run!");
        

        /**
         * 重点方法replay,此方法用来给当前子线程赋本地变量,返回的backup是此子线程原来就有的本地变量值(原生本地变量,下面会详细讲),
         * backup用于恢复数据(如果任务执行完毕,意味着该子线程会归还线程池,那么需要将其原生本地变量属性恢复)
         */
        Object backup = replay(captured);
        try 
            runnable.run(); // 执行异步逻辑
         finally 
            restore(backup); // 结合上面对于replay的解释,不难理解,这个方法就是用来恢复原有值的
        
    

根据上述代码,我们看到了TTL在异步任务执行前,会先进行赋值操作(就是拿着异步发生时捕获到的父线程的本地变量,赋给自己),当任务执行完,就恢复原生的自己本身的线程变量值。

下面来具体看这俩方法:

//下面的方法均属于TTL的静态内部类Transmittable

@Nonnull
    public static Object replay(@Nonnull Object captured) 
        @SuppressWarnings("unchecked")
        Map<TransmittableThreadLocal<?>, Object> capturedMap = (Map<TransmittableThreadLocal<?>, Object>) captured; //使用此线程异步时捕获到的父线程里的本地变量值
        Map<TransmittableThreadLocal<?>, Object> backup = new HashMap<TransmittableThreadLocal<?>, Object>(); //当前线程原生的本地变量,用于使用完线程后恢复用

        //注意:这里循环的是当前子线程原生的本地变量集合,与本方法相反,restore方法里循环这个holder是指:该线程运行期间产生的变量+父线程继承来的变量
        for (Iterator<? extends Map.Entry<TransmittableThreadLocal<?>, ?>> iterator = holder.get().entrySet().iterator();
             iterator.hasNext(); ) 
            Map.Entry<TransmittableThreadLocal<?>, ?> next = iterator.next();
            TransmittableThreadLocal<?> threadLocal = next.getKey();

            backup.put(threadLocal, threadLocal.get()); // 所有原生的本地变量都暂时存储在backup里,用于之后恢复用

            /**
             * 检查,如果捕获到的线程变量里,不包含当前原生变量值,则从当前原生变量里清除掉,对应的线程本地变量也清掉
             * 这就是为什么会将原生变量保存在backup里的原因,为了恢复原生值使用
             * 那么,为什么这里要清除掉呢?因为从使用这个子线程做异步那里,捕获到的本地变量并不包含原生的变量,当前线程
             * 在做任务时的首要目标,是将父线程里的变量完全传递给任务,如果不清除这个子线程原生的本地变量,
             * 意味着很可能会影响到任务里取值的准确性。
             *
             * 打个比方,有ttl对象tl,这个tl在线程池的某个子线程里存在对应的值2,当某个主线程使用该子线程做异步任务时
             * tl这个对象在当前主线程里没有值,那么如果不进行下面这一步的操作,那么在使用该子线程做的任务里就可以通过
             * 该tl对象取到值2,不符合预期
             */
            if (!capturedMap.containsKey(threadLocal)) 
                iterator.以上是关于transmittable-thread-local:解决线程池之间ThreadLocal本地变量传递的问题的主要内容,如果未能解决你的问题,请参考以下文章