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 DelegatingContextRunnable(task);
}
protected final Executor getDelegateExecutor() {
return delegate;
}
}
public Executor queryExecutor() {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor();
// 封装服务上下文的线程池修饰
return new DelegatingContextExecutorService(threadPoolExecutor