全链路追踪必备组件之 TransmittableThreadLocal 详解

ThreadLocal 线程传递 & TransmittableThreadLocal 源码解析

我们都知道 ThreadLocal 作为一种多线程处理手段,将数据限制在当前线程中,避免多线程情况下出现错误。

一般的使用场景大多会是服务上下文、分布式日志跟踪。

但是在业务代码中,为了提高响应速度,将多个复杂、长时间的计算或调用过程异步进行,让主线程可以先进行其他操作。像我们项目中最常用的就是 CompletableFuture 了,默认会使用预设的 ForkJoin ThreadPool 执行。

这也就引入了一个问题,如果保证 ThreadLocal 的信息能够传递异步线程?通过 ThreadLocal ?通过线程池? 通过 Runnable or Callable?

有些场景丢了就丢了,比如我们的服务上下文,一般都没有很严谨的处理 …

但是,如果是分布式追踪的场景,丢了就要累惨了。

注:以下代码仅保留关键代码,其余无关紧要则忽略

InheritableThreadLocal

InheritableThreadLocal 是 JDK 本身自带的一种线程传递解决方案。顾名思义,由当前线程创建的线程,将会继承当前线程里 ThreadLocal 保存的值。

其本质上是 ThreadLocal 的一个子类,通过覆写父类中创建初始化的相关方法来实现的。我们知道,ThreadLocal实际上是 Thread 中保存的一个 ThreadLocalMap 类型的属性搭配使用才能让广大 Javaer 直呼真香的,所以 InheritableThreadLocal 也是如此。

public class Thread implements Runnable {
   
    // 如果单纯使用 ThreadLocal,则 Thread 使用该属性值保存 ThreadLocalMap
    ThreadLocal.ThreadLocalMap threadLocals = null;
    // 否则使用该属性值
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
  
    private void init(ThreadGroup g, Runnable target, String name,
                        long stackSize, AccessControlContext acc) {
   
          Thread parent = currentThread();

          if (parent.inheritableThreadLocals != null)
              this.inheritableThreadLocals =
                  ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

      }
}

init 方法作为 Thread 初始化的核心方法,相关 ThreadLocal 已经全部摘出。如我们所见,仅仅就只是这一点改动。在创建线程时,如果当前线程的 inheritableThreadLocals 不为空,则根据 inheritableThreadLocals 创建出新的 InheritableThreadLocals 保存到新线程中。

Ps : ThreadLocal 作为老牌选手,默认都是使用时,初始化 Thread 的 threadLocals 属性。

只有像是 InheritableThreadLocal 这样的后辈,需要特殊处理一下。

public class InheritableThreadLocal<T> extends ThreadLocal<T> {
   
    
    protected T childValue(T parentValue) {
   
        return parentValue;
    }
  
    ThreadLocalMap getMap(Thread t) {
   
       return t.inheritableThreadLocals;
    }

   // Thread 中 ThreadLocalMap 不存在时的初始化动作,需要改为初始化 inheritableThreadLocals
    void createMap(Thread t, T firstValue) {
   
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

因此,原先 ThreadLocal 会从 Thread 的 threadLocals 获取 Map,那么 InheritableThreadLocal 就要从 inheritableThreadLocals 拿了。childValue() 用作从父线程中获取值,可以看到,这边是直接返回的,如果是复杂对象,就直接传引用了。当然,可以继承覆写该方法,浅拷贝、深拷贝不是手到擒来吗。

缺点

解决创建线程时的 ThreadLocal 传值的问题,但是项目中不可能一直创建新的线程,那么实在耗费资源。因此通用做法是线程复用,比如线程池呗。但是,相应的 ThreadLocal 的值就传递不过去了。

我们希望的是,异步线程执行任务的所使用的 ThreadLocal 值,是将任务提交给线程时主线程持有的。即从任务创建时传递到任务执行时

想想,如果我们在创建异步任务时,在任务代码外 ThreadLoca.get(),再在任务代码中首先执行 ThreadLocal.set() 就好了吧。对,确实可以,但是麻烦不?每个创建异步任务的地方都要写。

那就把它通用化处理一下。

RunnableWrapper/ CallableWrapper

假设按照服务上下文的场景举例,目前 HIS 中的执行异步操作的方案是定义一个 AsyncExecutor,并声明执行 Supplier 返回 CompletableFuture 的方法。

既然这样就可以对方法做一些改造,保证上下文的传递。

private static ThreadLocal<String> contextHolder = new ThreadLocal<>();

public static <T> CompletableFuture<T> invokeToCompletableFuture(Supplier<T> supplier, String errorMessage) {
   
    // 1.
    String context = contextHolder.get();
    Supplier<T> newSupplier = () -> {
   
         // 2.
        String origin = contextHolder.get();
        try {
   
            contextHolder.set(context);
            // 3.
            return supplier.get();
        } finally {
   
            // 4.
            contextHolder.set(origin);
            log.info(origin);
        }
    };
    return CompletableFuture.supplyAsync(newSupplier).exceptionally(e -> {
   
        throw new ServerErrorException(errorMessage, e);
    });
}

public static void main(String[] args) throws ExecutionException, InterruptedException {
   
    contextHolder.set("main");
    log.info(contextHolder.get());
    CompletableFuture<String> context = invokeToCompletableFuture(() -> test.contextHolder.get(), "error");
    log.info(context.get());
}

总得来说,就是在将异步任务派发给线程池时,对其做一下上下文传递的处理。

1.主线程获取上下文,传递给任务暂存。

​ 1 之后的操作都将是异步执行线程操作的。

2.异步执行线程将原有上下文取出,暂时保存。并将主线程传递过来的上下文设置。

3.执行异步任务

4.将原有上下文设置回去。

可以看到一般并不会在异步线程执行完任务之后直接进行 remove。而是一开始取出原上下文(可能为 NULL,也可能是线程创建时 InheritableThreadLocal 继承过来的值。当然后续也会被清除的),并在任务执行完成放回去。这样的方式可以说是异步 ThreadLocal 处理的标准范式(大佬说的)。既起到了显式清除主线程带来的上下文,也避免了如果线程池的拒绝策略为 CallerRunsPolicy,后续上下文丢失的问题。

Supplier 不算是典型例子,更为典型的应该是 Runnable 和 Callable。不过一以贯之,都是修饰一下,再丢给线程池。

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) {
   
  delegate.execute(wrap(task));
}

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

后续,使用线程池执行异步任务的时候,事先对任务进行封装代理即可。

不过,还是比较麻烦。自定义的线程池,需要显式处理任务。而且更严谨的做法,不同业务场景之间的线程池应该是隔离的,以免受到影响,就比如 Hystrix 的线程池。

每一个线程池都要处理就麻烦了。所以换个思路,代理线程池。

DelegaingExecutor

这个就不多说了,实际很简单,就照搬我们 Context 相关类库。

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
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值