这是我参与2022首次更文挑战的第20天,活动详情查看:2022首次更文挑战
一、背景
上文说到父子线程传递本地变量可以通过InheritableThreadlocoal
进行传递,但是如果采用线程池,不一定能传递,因为在线程在线程池中的存在不是每次使用都会进行创建,InheritableThreadlocal
是在线程初始化时intertableThreadLocals=true
才会进行拷贝传递。所以若本次使用的子线程是已经被池化的线程,从线程池中取出线下进行使用,是没有经过初始化的过程,也就不会进行父子线程的本地变量拷贝。
由于在日常应用场景中,绝大多数都是会采用线程池的方式进行资源的有效管理。目前知道的阿里有一个开源项目就是为了解决这个问题ThansmittableThreadLocal
。
二、简介
在使用线程池等会池化复用线程的执行组件情况下,提供ThreadLocal
值的传递功能,解决异步执行时上下文传递的问题。
JDK
的InheritableThreadLocal
类可以完成父线程到子线程的值传递。但对于使用线程池等会池化复用线程的执行组件的情况,线程由线程池创建好,并且线程是池化起来反复使用的;这时父子线程关系的ThreadLocal
值传递已经没有意义,应用需要的实际上是把 任务提交给线程池时的ThreadLocal
值传递到 任务执行时。
本章主要介绍使用线程池场景下的问题,TransmittableThreadLocal
还有很多其他的应用场景。
三、基本使用
创建一个线程池,通过TtlExecutors
工具类的包装方法,获取到经过ttl
框架封装的线程池对象。然后创建需要传递给线程池的本地变量。
①首次调用,这时候线程池中还未有线程,就算不使用TTL
也可以通过InheritableThreadLocal
获取到父线程的本地变量。
②当第二次调用时,由于使用的是单一线程的线程池,这时候是复用了上面创建的线程,所以这时通过inheritableThreadLocal
逻辑上是获取不到第二次赋值的本地变量的。而应该是获取到第一次线程创建时赋值的变量。而通过使用ttl
,从结果来看,这里输出了第二次赋值的本地变量值。
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newSingleThreadExecutor();
executorService = TtlExecutors.getTtlExecutorService(executorService);
TransmittableThreadLocal<String> username = new TransmittableThreadLocal<>();
// ①
username.set("zhangShang");
executorService.submit(new Runnable() {
@Override
public void run() {
log.info(username.get());
}
});
// ②
username.set("liSi");
executorService.submit(new Runnable() {
@Override
public void run() {
log.info(username.get());
}
});
}
输出结果为:
zhangShang
liSi
四、原理
从定义来看,TransimittableThreadLocal
继承于InheritableThreadLocal
,并实现TtlCopier
接口,它里面只有一个copy
方法。所以主要是对InheritableThreadLocal
的扩展。
public class TransmittableThreadLocal<T> extends InheritableThreadLocal<T> implements TtlCopier<T>
在TransimittableThreadLocal
中添加holder
属性。这个属性的作用就是被标记为具备线程传递资格的对象都会被添加到这个对象中。要标记一个类,比较容易想到的方式,就是给这个类新增一个Type
字段,还有一个方法就是将具备这种类型的的对象都添加到一个静态全局集合中。之后使用时,这个集合里的所有值都具备这个标记。
holder
本身是一个InheritableThreadLocal
对象- 这个
holder
对象的value
是WeakHashMap<TransmittableThreadLocal<Object>, ?>
2.1WeekHashMap
的value
总是空,且不可能被使用。
2.2WeekHasshMap
支持value=null
重写了childValue
方法,实现上直接将父线程的属性作为子线程的本地变量对象。
private static InheritableThreadLocal<WeakHashMap<TransmittableThreadLocal<Object>, ?>> holder = new InheritableThreadLocal<WeakHashMap<TransmittableThreadLocal<Object>, ?>>() {
@Override
protected WeakHashMap<TransmittableThreadLocal<Object>, ?> initialValue() {
return new WeakHashMap<TransmittableThreadLocal<Object>, Object>();
}
@Override
protected WeakHashMap<TransmittableThreadLocal<Object>, ?> childValue(WeakHashMap<TransmittableThreadLocal<Object>, ?> parentValue) {
return new WeakHashMap<TransmittableThreadLocal<Object>, Object>(parentValue);
}
};
应用代码是通过TtlExecutors
工具类对线程池对象进行包装。工具类只是简单的判断,输入的线程池是否已经被包装过、非空校验等,然后返回包装类ExecutorServiceTtlWrapper
。根据不同的线程池类型,有不同和的包装类。
public static ExecutorService getTtlExecutorService(ExecutorService executorService) {
if (TtlAgent.isTtlAgentLoaded() || executorService == null || executorService instanceof TtlEnhanced) {
return executorService;
}
return new ExecutorServiceTtlWrapper(executorService);
}
进入包装类ExecutorServiceTtlWrapper
。可以注意到不论是通过ExecutorServiceTtlWrapper#submit
方法或者是ExecutorTtlWrapper#execute
方法,都会将线程对象包装成TtlCallable
或者TtlRunnable
,用于在真正执行run
方法前做一些业务逻辑。
在ExecutorServiceTtlWrapper
实现submit
方法。间接调用对应的submit
执行方法。
public <T> Future<T> submit(Callable<T> task) {
return executorService.submit(TtlCallable.get(task));
}
在ExecutorTtlWrapper
实现execute
方法,间接调用对应的execute
执行方法。
public void execute(Runnable command) {
executor.execute(TtlRunnable.get(command));
}
所以,重点的核心逻辑应该是在TtlCallable#call()
或者TtlRunnable#run()
中。以下以TtlCallable
为例,TtlRunnable
同理类似。在分析call()
方法之前,先看一个类Transmitter
- 捕获当前线程中的是所有
TransimittableThreadLocal
和注册ThreadLocal
的值。 - 捕获
TransimittableThreadLocal
的值,将holder
中的所有值都添加到HashMap
后返回。 - 捕获注册的
ThreadLocal
的值,也就是原本线程中的ThreadLocal
,可以注册到TTL
中,在进行线程池本地变量传递时也会被传递。 - 将捕获到的本地变量进行替换子线程的本地变量,并且返回子线程现有的本地变量副本
backup
。用于在执行run/call
方法之后,将本地变量副本恢复。
public static class Transmitter {
// 1.
public static Object capture() {
return new Snapshot(captureTtlValues(), captureThreadLocalValues());
}
// 2.
private static HashMap<TransmittableThreadLocal<Object>, Object> captureTtlValues() {
// ...
return ttl2Value;
}
// 3.
private static HashMap<ThreadLocal<Object>, Object> captureThreadLocalValues() {
HashMap<ThreadLocal<Object>, Object> threadLocal2Value =
new HashMap<ThreadLocal<Object>, Object>();
return threadLocal2Value;
}
// 4.
public static Object replay(Object captured) {
return new Snapshot(replayTtlValues(capturedSnapshot.ttl2Value),
replayThreadLocalValues(capturedSnapshot.threadLocal2Value));
}
替换TransmittableThreadLocal
,将原线程中线程变量先拷贝一份,然后设置ttl
值。
- 若出现调用线程中不存在某个线程变量,而线程池中线程有,则删除线程池中对应的本地变量。
- 将捕获的
TTL
值打入线程池获取到的线程TTL
中。 - 这是一个扩展点,调用
TTL
的beforeExecute
方法,在线程变量拷贝的前置操作。默认实现为空 - 清除单线线程的所有
TTL
和TL
,并返回清除之前的backup
private static HashMap<TransmittableThreadLocal<Object>, Object> replayTtlValues(HashMap<TransmittableThreadLocal<Object>, Object> captured) {
// 创建副本backup
HashMap<TransmittableThreadLocal<Object>, Object> backup =
new HashMap<TransmittableThreadLocal<Object>, Object>();
for (Iterator<TransmittableThreadLocal<Object>> iterator = holder.get().keySet().iterator(); iterator.hasNext(); ) {
// 1.
if (!captured.containsKey(threadLocal)) {
iterator.remove();
threadLocal.superRemove();
}
}
// 2.
setTtlValuesTo(captured);
// 3.
doExecuteCallback(true);
return backup;
}
// 4.
public static Object clear() {
// ...
}
在执行结束之后进行现场的还原。
public static void restore(Object backup) {
// ..
}
还原线程具体的接口,doExecuteCallback
是一个扩展点,调用TTL
的afterExecute
,用来自定义一些还原现场的后置处理,然后将本地变量恢复成备份的版本。
private static void restoreTtlValues(HashMap<TransmittableThreadLocal<Object>, Object> backup) {
doExecuteCallback(false);
setTtlValuesTo(backup);
}
进入TtlCallable#call()
方法。调用replay方法将捕获到的当前线程的本地变量,传递给线程池线程的本地变量,并且获取到线程池线程覆盖之前的本地变量副本。在finally
中通过相应的方法进行使用拷贝的副本,对现场进行还原。从以下代码可以看出,通过类似于委托的方式,调用到了具体的callable
的执行方法。
public V call(){
Object backup = replay(captured);
try {
return callable.call();
} finally {
restore(backup);
}
}
到这基本上线程池方式传递本地变量的核心代码已经大概看完了。总的来说在创建TtlCallable
对象是,调用capture()
方法捕获调用方的本地线程变量,在call()
执行时,将捕获到的线程变量,替换到线程池所对应获取到的线程的本地变量中,并且在执行完成之后,将其本地变量恢复到调用之前。
总结
TTL
在实现逻辑上是经过了一层对spring
的线程池框架进行包装,当将任务添加到对应线程接管时,进入到具体的包装方法中,进行原线程的线程变量拷贝后,赋值委托主线程吃线程变量,当线程任务执行结束后,对线程变量的现场通过最初拷贝的值进行还原。在阿里的官方文档中,还存在一种通过探针的方式,来进行ttl
的使用,这种情况下就可以在不修改原有业务代码的基础上,进行线程池线程本地变量的传递,有兴趣的朋友可以去看看。