Bootstrap

TransmittableThreadLocal

概述

在使用线程池等会池化复用线程的执行组件情况下,提供ThreadLocal值的传递功能,解决异步执行时上下文传递的问题。

ThreadLocal使用场景

•针对线程不安全的例如SimpleDateFormat使用时能够支持多线程状态下的安全使用。同时不需要实例化过多实例和使用锁。

    private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>() {
        @Override
        protected DateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        }
    };

    public static Date parse(String dateStr) throws ParseException {
        return threadLocal.get().parse(dateStr);
    }

    public static String format(Date date) {
        return threadLocal.get().format(date);
    }

•上下文调用中屏蔽过多的方法栈中的传参(例如用户信息,客户端信息等存入​​ThreadLocal​​,那么当前线程在任何地方需要时,都可以使用)

public class ClientThreadLocal {

    private static final ThreadLocal<Map<String, Object>> THREAD_LOCAL = ThreadLocal.withInitial(Maps::newHashMap);

    public static void set(Map<String, Object> clientMap) {
   
        THREAD_LOCAL.set(clientMap);
    }

    public static void remove() {
        THREAD_LOCAL.remove();
    }
     /**
     * 此方法可以支持在当前线程内任意地方获取客户端信息
     * 而不需要将clientInfo一直传递在上下文中
     * @return
     */
    public static Map<String, Object> get() {
        return THREAD_LOCAL.get();
    }
}

InheritableThreadLocal使用场景

    private final static ThreadLocal<String> threadLocal = new ThreadLocal<>();

    private final static InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();


    public static void main(String[] args) {
        threadLocal.set("threadLocal");
        Runnable runnable1 = () -> System.out.println(Thread.currentThread().getName()+":"+threadLocal.get());
        new Thread(runnable1).start();


        inheritableThreadLocal.set("inheritableThreadLocal");
        Runnable runnable2 = () -> System.out.println(Thread.currentThread().getName()+":"+inheritableThreadLocal.get());
        new Thread(runnable2).start();
    }





很明显ThreadLocal的子类InheritableThreadLocal在ThreadLocal的基础上,解决了和线程相关的副本从父线程向子线程传递的问题。

TransmittableThreadLocal使用场景

InheritableThreadLocal的原理是通过创建线程的时候,会把父线程当前的inheritableThreadLocals拷贝过去。但是业务会存在很多使用线程池的场景,线程池的线程是池化和复用的。这种情况下,运行一段时间线程池,会导致线程一直获取不到最新的InheritableThreadLocal。从而导致部分信息丢失,TransmittableThreadLocal就是用来解决这个问题的。

package com.jd.app.lambda.test.service;
import com.alibaba.ttl.TransmittableThreadLocal;
import com.alibaba.ttl.TtlRunnable;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

@Slf4j
@RestController
public class TempTest4 {

    private static    InheritableThreadLocal inheritableThreadLocal = new InheritableThreadLocal();

    private static TransmittableThreadLocal transmittableThreadLocal  = new TransmittableThreadLocal();

    private static ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2,2,3000, TimeUnit.MILLISECONDS,new ArrayBlockingQueue(100));

    public static void main(String[] args) {

        Runnable runnable2 = () -> System.out.println(Thread.currentThread().getName()+":"+inheritableThreadLocal.get());


        inheritableThreadLocal.set(Thread.currentThread()+"inheritableThreadLocal");
        threadPoolExecutor.submit(runnable2);
        threadPoolExecutor.submit(runnable2);

        inheritableThreadLocal.set(Thread.currentThread()+"again inheritableThreadLocal");
        /**
         * 这里很明显没有覆盖
         * 输出还是第一次set进去的值
         */
        threadPoolExecutor.submit(runnable2);


        Runnable runnable3 = () -> System.out.println(Thread.currentThread().getName()+":"+transmittableThreadLocal.get());
        transmittableThreadLocal.set(Thread.currentThread()+"transmittableThreadLocal");
        threadPoolExecutor.submit(TtlRunnable.get(runnable3));
        threadPoolExecutor.submit(TtlRunnable.get(runnable3));

        transmittableThreadLocal.set(Thread.currentThread()+"again transmittableThreadLocal");
        /**
         * 这里覆盖了  异步执行的时候能够正常输出新值
        */
        threadPoolExecutor.submit(TtlRunnable.get(runnable3));
    }
}







TransmittableThreadLocal使用方式

一.侵入式代码改造

1.Runnable
    Runnable runnable = () -> System.out.println(Thread.currentThread().getName()+":"+transmittableThreadLocal.get());
    transmittableThreadLocal.set(Thread.currentThread()+"runnable transmittableThreadLocal");
    threadPoolExecutor.submit(TtlRunnable.get(runnable));
2.Callable
      Callable callable = () -> {
            System.out.println(Thread.currentThread().getName()+":"+transmittableThreadLocal.get());
            return null;
        };
        transmittableThreadLocal.set(Thread.currentThread()+"callable transmittableThreadLocal");
        threadPoolExecutor.submit(TtlCallable.get(callable));
3.线程池
     //普通线程池
     Runnable runnable = () -> System.out.println(Thread.currentThread().getName()+":"+transmittableThreadLocal.get());
        Callable callable = () -> {
            System.out.println(Thread.currentThread().getName()+":"+transmittableThreadLocal.get());
            return null;
        };

        transmittableThreadLocal.set(Thread.currentThread()+"TtlExecutors transmittableThreadLocal");
        ExecutorService executorService = TtlExecutors.getTtlExecutorService(threadPoolExecutor);
        executorService.submit(runnable);
        executorService.submit(callable);
        
        
        
    //stream并行流自定义forkjoinpool 
    List<Integer> integerList= IntStream.range(1,1000).boxed().collect(Collectors.toList());
    ForkJoinPool customThreadPool = new ForkJoinPool(4);
    ExecutorService executorService = TtlExecutors.getTtlExecutorService(customThreadPool);
    Integer actualTotal = executorService.submit(
    () -> integerList.parallelStream().reduce(0, Integer::sum)).get();

    //CompletableFuture下自定义forkjoinpool
    CompletableFuture future3 = CompletableFuture.runAsync(()->{
       System.out.println("999");
    },TtlExecutors.getTtlExecutorService(threadPoolExecutor));
       

二.非侵入式agent改造

根据jar包地址,启动脚本添加相关路径jar。

-javaagent:path/to/transmittable-thread-local-2.x.y.jar

添加并启动正常后,可以不修改业务代码修饰任务或者线程池,直接使用。默认修饰了jdk自带的相关任务或者线程池如下。

1.java.util.concurrent.ThreadPoolExecutorjava.util.concurrent.ScheduledThreadPoolExecutor

2.java.util.concurrent.ForkJoinTask(对应的执行器组件是java.util.concurrent.ForkJoinPool

3.java.util.TimerTask的子类(对应的执行器组件是java.util.Timer

相关agent技术文档说明:

javaagent: JavaAgent 技术原理及简单实现 - 简书

ttl使用javaagent: TransmittableThreadLocal通过javaAgent技术实现线程传递(并且支持ForkJoin) - 简书

异步执行上下文传递标准范式

在上面的TransmittableThreadLocal使用场景说明的时候,已经确认到线程池的线程是复用的。所以按照InheritableThreadLocal的想法在创建线程的时候操作相关需要的上下文信息是不合理的。使用线程池的时候,需要提交任务。所以在提交任务的时候,把上下文信息打包给到任务是合理的。

一.异步执行传递上下文标准范式

    private static ThreadLocal<String> contextHolder = new ThreadLocal<>();
    
    public static <T> CompletableFuture<T> invokeToCompletableFuture(Supplier<T> supplier) {
        contextHolder.set("main");
        // 第一步 主线程获取上下文,传递给任务暂存。
        String context = contextHolder.get();
        // 异步任务逻辑执行
        return CompletableFuture.supplyAsync(() -> {
            // 第二步 异步执行线程将原有上下文取出,暂时保存。
            String origin = contextHolder.get();
            try {
                contextHolder.set(context);
                // 第三步 执行异步任务 这里已经是异步执行的时候设置的context值了
                return supplier.get();
            } finally {
                // 第四步 原来的异步线程值重新放回去
                contextHolder.set(origin);
                log.info(origin);
            }
        });
    }}

二.基于标准范式的任务包装

    private static ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(3,3,3000, TimeUnit.MILLISECONDS,new ArrayBlockingQueue(100));
    private static ThreadLocal<String> threadLocal = new ThreadLocal<>();

    public final static class PackageRunnable implements Runnable {

        private  Runnable runnable;

        private  String mainContext;

        public PackageRunnable(Runnable runnable,String mainContext) {

            this.runnable = runnable;
            this.mainContext = mainContext;
        }

        public PackageRunnable(Runnable runnable) {
            // 修饰原有的任务,并保存当前线程的值
            this(runnable, threadLocal.get());
        }

        //异步执行
        public void run() {
            //异步执行线程将原有上下文取出,暂时保存。
            String originalContext = threadLocal.get();
            try {
                //主线程传递过来的上下文设置进来。
                threadLocal.set(mainContext);
                runnable.run();
            } finally {
                //将原有上下文设置刷新回去。
                threadLocal.set(originalContext);
            }
        }
    }

    public final void execute(Runnable runnable) {
        // 递交给真正的执行线程池前,对任务进行修饰
        threadPoolExecutor.execute(pack(runnable));
    }

    protected static final Runnable pack(Runnable runnable) {
        return new PackageRunnable(runnable);
    }

三.基于标准范式的线程池包装

    static PackageThreadPool packageThreadPool = new PackageThreadPool(3,3,3000,3000);
    public static class PackageThreadPool extends ThreadPoolExecutor {

        public PackageThreadPool(int corePoolSize, int maximumPoolSize, int queueSize, long keepAliveTime) {
            super(corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.SECONDS, new ArrayBlockingQueue(queueSize <= 0 ? 10: queueSize), Executors.defaultThreadFactory());
        }

        //包装线程池 本质也是包装任务
        @Override
        public void execute(Runnable runnable) {
            super.execute(pack(runnable));
        }

    }

    public static void main(String[] args) {
        Runnable runnable = () -> System.out.println(Thread.currentThread().getName());
        packageThreadPool.submit(runnable);
    }

TransmittableThreadLocal源码解析



TransmittableThreadLocal官方文档时序图











 public final class TtlCallable<V> implements Callable<V>, TtlWrapper<Callable<V>>, TtlEnhanced, TtlAttachments {
        // 保存父线程的 ThreadLocal 快照
        private final AtomicReference<Object> capturedRef;
        // 实际执行任务
        private final Callable<V> callable;
        // 判断是否执行完,清除任务所保存的 ThreadLocal 快照
        private final boolean releaseTtlValueReferenceAfterCall;

        private TtlCallable(@NonNull Callable<V> callable, boolean releaseTtlValueReferenceAfterCall) {
            // 1.创建时, 从 Transmitter 抓取快照
            this.capturedRef = new AtomicReference<Object>(capture());
            this.callable = callable;
            this.releaseTtlValueReferenceAfterCall = releaseTtlValueReferenceAfterCall;
        }

        @Override
        public V call() throws Exception {
            //1.获取父线程传递下来的上下文。临时保存在captured。
            Object captured = capturedRef.get();
            // 如果 releaseTtlValueReferenceAfterCall 为 true,则在执行线程取出快照后清除。
            if (captured == null || releaseTtlValueReferenceAfterCall && !capturedRef.compareAndSet(captured, null)) {
                throw new IllegalStateException("TTL value reference is released after call!");
            }
            // 2.使用 Transmitter 将快照重做到当前执行线程,并将原来的值取出 这里将父线程传递的captured也就是
            //上下文
            // 并返回异步线程原来持有的上下文
            Object backup = replay(captured);
            try {
                // 3.执行任务 执行任务过程使用的就会使用父线程传递下来的上下文
               return callable.call();
            } finally {
                // 4.Transmitter 重新将异步线程原来持有的上下文放到异步线程里
                restore(backup);
            }
        }
    }

可以看到相关源码符合标准范式,下面具体分析三个主要方法分别对应Transmittee的capture,replay,restore方法

1.capture

 public HashMap<TransmittableThreadLocal<Object>, Object> capture() {
     final HashMap<TransmittableThreadLocal<Object>, Object> ttl2Value = newHashMap(holder.get().size());
     for (TransmittableThreadLocal<Object> threadLocal : holder.get().keySet()) {
       //可以看到这里将holder下所有的TransmittableThreadLocal作为key TransmittableThreadLocal当前线程持有的value作为
       //value进行重新put。当前线程也就是父线程。
       ttl2Value.put(threadLocal, threadLocal.copyValue());
     }
     return ttl2Value;
}

2.replay

  public HashMap<TransmittableThreadLocal<Object>, Object> replay(@NonNull HashMap<TransmittableThreadLocal<Object>, Object> captured) {
    final HashMap<TransmittableThreadLocal<Object>, Object> backup = newHashMap(holder.get().size());

    for (final Iterator<TransmittableThreadLocal<Object>> iterator = holder.get().keySet().iterator(); iterator.hasNext(); ) {
       TransmittableThreadLocal<Object> threadLocal = iterator.next();

      // backup 这里将holder下所有的TransmittableThreadLocal以及当前异步线程所持有的value重新赋值
      backup.put(threadLocal, threadLocal.get());

      // clear the TTL values that is not in captured
      // avoid the extra TTL values after replay when run task
      if (!captured.containsKey(threadLocal)) {
          iterator.remove();
          threadLocal.superRemove();
         }
      }

     // 替换父线程传递下来的value值到当前异步线程
     setTtlValuesTo(captured);

     doExecuteCallback(true);

     return backup;
}

3.restore

 public void restore(@NonNull HashMap<TransmittableThreadLocal<Object>, Object> backup) {
    // call afterExecute callback
    doExecuteCallback(false);

    for (final Iterator<TransmittableThreadLocal<Object>> iterator = holder.get().keySet().iterator(); iterator.hasNext(); ) {
        TransmittableThreadLocal<Object> threadLocal = iterator.next();

        // clear the TTL values that is not in backup
        // avoid the extra TTL values after restore
        if (!backup.containsKey(threadLocal)) {
             iterator.remove();
             threadLocal.superRemove();
          }
        }

      // 刷新当前异步线程传递下来的上下文值到当前异步线程
      setTtlValuesTo(backup);
}

4.holder的设计

从上面的相关逻辑可以看到holder这个设计,是用来注册所有的TransmittableThreadLocal。那么为什么需要这个holder来注册所有的TransmittableThreadLocal呢?

一.支持注册多个TransmittableThreadLocal

比如说单个请求同时需要传递客户端信息,用户登陆信息等等,那么这里就需要声明一个以上的TransmittableThreadLocal,并且异步上下文传递的时候需要将所有的TransmittableThreadLocal都进行传递。

二.线程级别的缓存

我们知道ThreadLocal和InheritableThreadLocal怎么做到线程级别的缓存,是通过Thread类的属性来绑定的。setValue的时候通过Thread的map来存储ThreadLocal和InheritableThreadLocal分别作为key的value值。


     /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

    /*
     * InheritableThreadLocal values pertaining to this thread. This map is
     * maintained by the InheritableThreadLocal class.
     */
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

显然我们不可能拓展Thread这个类,成本太高且不可控。所以设计这个holder来作为一个线程级别的缓存缓存当前线程请求下所有的TransmittableThreadLocal。同样的在setValue的时候将相关TransmittableThreadLocal注册到这个static的holder上。

static用来保证当前线程 无论创建多少次TransmittableThreadLocal,维护的都是同一个缓存holder。


    public final void set(T value) {
        if (!disableIgnoreNullValueSemantics && value == null) {
            // may set null to remove value
            remove();
        } else {
            super.set(value);
            addThisToHolder();
        }
    }
    //注册到holder
    private void addThisToHolder() {
        if (!holder.get().containsKey(this)) {
            holder.get().put((TransmittableThreadLocal<Object>) this, null); // WeakHashMap supports null value.
        }
    }

可以看到这个holder作为一个WeakHashMap的key是当前TransmittableThreadLocal对象。

 private static final InheritableThreadLocal<WeakHashMap<TransmittableThreadLocal<Object>, ?>> holder =
            new InheritableThreadLocal<WeakHashMap<TransmittableThreadLocal<Object>, ?>>() {
                @Override
                protected WeakHashMap<TransmittableThreadLocal<Object>, ?> initialValue() {
                    return new WeakHashMap<>();
                }

                @Override
                protected WeakHashMap<TransmittableThreadLocal<Object>, ?> childValue(WeakHashMap<TransmittableThreadLocal<Object>, ?> parentValue) {
                    return new WeakHashMap<>(parentValue);
                }
 };
三.弱引用机制

为了确保不会造成内存泄漏,TransmittableThreadLocal作为key被弱引用(发生gc就会回收该TransmittableThreadLocal)

其实这里的概念和ThreadLocal以及InheritableThreadLocal里面的设计是一致的。

Thread -> ThreadLocal.ThreadLocalMap -> Entry[] -> Enrty -> key(threadLocal对象)和value
Thread -> InheritableThreadLocal.ThreadLocalMap -> Entry[] -> Enrty -> key(inheritableThreadLocal对象)和value
Thread -> holder.ThreadLocalMap -> Entry[] -> Enrty -> key(transmittableThreadLocal对象)和value

如上,如果是强引用的话,Thread很难销毁的情况下,会一直持有相关的对象。就会存在内存泄漏。

所以弱引用情况下,gc的时候就会直接回收所在类的ThreadLocal,InheritableThreadLocal,TransmittableThreadLocal。

如果这些ThreadLocal,InheritableThreadLocal,TransmittableThreadLocal不存在被持有的情况下。

弱引用是一种设计上的弥补措施。用来防止因为线程一直不销毁的情况下发生的key的内存泄漏。



5.remove机制

既然上面的key不会发生内存泄漏,为什么会有remove方法可以调用呢?

是不是 Key 持有的是 threadlocal 对象的弱引用就一定不会发生内存泄漏呢?

使用不当情况下,仍然会发生内存泄漏:

当 threadlocal 使用完后,将栈中的 threadlocal 变量置为 null,threadlocal 对象下一次 GC 会被回收,那么 Entry 中的与之关联的弱引用 key 就会变成 null,如果此时当前线程还在运行,那么 Entry 中的 key 为 null 的 Value 对象并不会被回收(存在强引用),这就发生了内存泄漏,当然这种内存泄漏分情况,如果当前线程执行完毕会被回收,那么 Value 自然也会被回收, 但是如果使用的是线程池呢,线程跑完任务以后放回线程池(线程没有销毁,不会被回收),Value 会一直存在,这就发生了内存泄漏。

ThreadLocal如何更好的降低内存泄漏的风险呢?

ThreadLocal 为了降低内存泄露的可能性,在 set,get,remove 的时候都会清除此线程 ThreadLocalMap 里 Entry 数组中所有 Key 为 null 的 Value。所以,当前线程使用完 threadlocal 后, 我们可以通过调用 ThreadLocal 的 remove 方法进行清除从而降低内存泄漏的风险。

private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;

            // expunge entry at staleSlot
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;

            // Rehash until we encounter null
            Entry e;
            int i;
            for (i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
                if (k == null) {
                //value设置为空,防止泄漏。
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                    int h = k.threadLocalHashCode & (len - 1);
                    if (h != i) {
                        tab[i] = null;

                        // Unlike Knuth 6.4 Algorithm R, we must scan until
                        // null because multiple entries could have been stale.
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            return i;
        }

另外remove方法可以防止串上下文。可以在使用过后强制remove,下次该线程执行的时候有自己的上下文信息。



个人结论

个人愚见,TransmittableThreadLocal的使用是有成本的,它只适合异步执行的时候上下文的传递,解决线程池场景下ThreadLocal和InheritableThreadLocal不能正确传递上下文的问题。

但是并不适合传递之后的变量涉及到并发修改的场景。比如同一请求下,传递到异步线程后,该异步线程发起多线程并发修改这个变量,收集信息。这个是不合理的。

另外改造成本,包括

如果是agent改造,需要注意版本的升级,比如启动脚本和maven项目引用的版本后续不一致,没有覆盖的线程池的场景,需要代码中另做改造。

如果是代码改造,需要注意识别并改造所有涉及到线程池的场景,包括业务线程池,Hystrix这类中间件线程池,jdk并行流,CompletableFuture等涉及到的ForkJoinPool等线程池。



参考文档:

GitHub - alibaba/transmittable-thread-local: 📌 TransmittableThreadLocal (TTL), the missing Java™ std lib(simple & 0-dependency) for framework/middleware, provide an enhanced InheritableThreadLocal that transmits values between threads even using thread pooling components.

Java进阶之路 - 知乎

;