关于 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 就是用来解决这个问题的。

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);
        
        
        
    //自定义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.ThreadPoolExecutor 和 java.util.concurrent.ScheduledThreadPoolExecutor

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

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

相关 agent 技术文档说明:

javaagent: https://www.jianshu.com/p/d573456401eb

ttl 使用 javaagent: https://www.jianshu.com/p/dfec3d3c1ba8

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

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

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

    private static ThreadLocal<String> contextHolder = new ThreadLocal<>();
    
    @GetMapping("/test")
    public void test() {
        contextHolder.set("main");
        CompletableFuture<String> context = invokeToCompletableFuture(() -> contextHolder.get(), "error");
        try {
            log.info(context.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
    
    public static <T> CompletableFuture<T> invokeToCompletableFuture(Supplier<T> supplier, String errorMessage) {
        // 第一步 主线程获取上下文,传递给任务暂存。
        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);
            }
        }).exceptionally(e -> {
            e.printStackTrace();
            return null;
        });
    }

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

 private static ThreadLocal<Optional<String>> ContextHolder = new ThreadLocal<>();
 
 public final class DelegatingContextRunnable implements Runnable {

        private final Runnable delegate;

        private final Optional<String> delegateContext;

        public DelegatingContextRunnable(Runnable delegate,
                                         Optional<String> context) {
            assert delegate != null;
            assert context != null;

            this.delegate = delegate;
            this.delegateContext = context;
        }

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

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

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

    protected final Runnable wrap(Runnable task) {
        return new DelegatingContextRunnable(task);
    }


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

//包装线程池 本质也是包装任务
    public class DelegatingContextExecutor implements Executor  {

        private final Executor delegate;


        public DelegatingContextExecutor(Executor delegateExecutor) {
            this.delegate = delegateExecutor;
        }

        public final void execute(Runnable task) {
            delegate.execute(wrap(task));
        }

        protected final Runnable wrap(Runnable task) {
            return new DelegatingContextRunnable(task);
        }

        protected final Executor getDelegateExecutor() {
            return delegate;
        }
    }
    // 自定义的线程池,用于执行项目中的异步任务
    public Executor queryExecutor() {
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(3,3,3000, TimeUnit.MILLISECONDS,new ArrayBlockingQueue(100));
        // 封装服务上下文的线程池修饰
        return new DelegatingContextExecutor(threadPoolExecutor);
    }

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 呢?

1. 支持注册多个 TransmittableThreadLocal

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

2. 线程级别的缓存

我们知道 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);
                }
 };

3. 弱引用机制

为了确保不会造成内存泄漏,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,下次该线程执行的时候有自己的上下文信息。

官方文档: https://github.com/alibaba/transmittable-thread-local

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值