前提
使用阿里巴巴的transmittable-thread-local可以让外部线程的ThreadLocal变量副本在线程池中也可以访问到,并且在请求结束后会把线程池中的属于外部线程的ThreadLocal变量清除掉,恢复原本的线程池中的线程ThreadLocal信息。
Transmittable-thread-local有两种使用方式
首先引入jar包
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
<version>2.11.4</version>
</dependency>
方案一:使用TransmittableThreadLocal并且需要使用TtlRunnable和TtlCallable修饰传入线程池的Runnable和Callable
官网示例
TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>();
// =====================================================
// 在父线程中设置
context.set("value-set-in-parent");
Callable call = new CallableTask();
// 额外的处理,生成修饰了的对象ttlCallable
Callable ttlCallable = TtlCallable.get(call);
executorService.submit(ttlCallable);
// =====================================================
// Call中可以读取,值是"value-set-in-parent"
String value = context.get();
方案二:使用TransmittableThreadLocal并且使用工具类TtlExecutors修饰线程池,避免每次都要修饰Runnale或者Callable
官网示例:
ExecutorService executorService = ...
// 额外的处理,生成修饰了的对象executorService
executorService = TtlExecutors.getTtlExecutorService(executorService);
TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>();
// =====================================================
// 在父线程中设置
context.set("value-set-in-parent");
Runnable task = new RunnableTask();
Callable call = new CallableTask();
executorService.submit(task);
executorService.submit(call);
// =====================================================
// Task或是Call中可以读取,值是"value-set-in-parent"
String value = context.get();
问题
我们这里的场景是使用了TransmittableThreadLocal,但是并没有使用TtlRunnable和TtlCallable修饰传入线程池的Runnable和Callable,也没有使用工具类TtlExecutors修饰线程池,这样导致外部线程的ThreadLocal变量永远留在了线程池里面,这样在线程池里根据ThreadLocal信息取数据的时候取到了错误的数据。
看一下TransmittableThreadLocal
public class TransmittableThreadLocal<T> extends InheritableThreadLocal<T> implements TtlCopier<T> {
它继承自InheritableThreadLocal,而InheritableThreadLocal是会在创建子线程的时候继承父线程的InheritableThreadLocal变量的,具体可以看
java.util.concurrent.Executors.DefaultThreadFactory#newThread
线程初始化时有这么一段代码
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
//…
Thread parent = currentThread();
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
//…
}
parent是外部线程
inheritThreadLocals默认是true
因此,创建的子线程会留有父线程的InheritableThreadLocal变量副本,并且一般情况下不会被清除。
在我们的项目里问题就是比如线程池中初始线程数为0,然后A请求进来新建了一个线程,并且设置TransmittableThreadLocal值为1,而B请求进来不需要再创建线程,因为之前A请求创建好的线程池线程空闲了,正好被B请求拿来用,那么原本B请求的TransmittableThreadLocal对应值应该为2,但是因为线程是A请求创建的,并且TransmittableThreadLocal已经被A请求设置为1了,所以在线程池线程中会取到错误的TransmittableThreadLocal变量。
因为我们会根据这个变量去数据库取数据,所以取到了错误的数据。
解决方法
解决方案一:按照文档使用工具类TtlExecutors修饰线程池
即
executorService = TtlExecutors.getTtlExecutorService(executorService);
它会使用ExecutorTtlWrapper包装传入的线程池,而ExecutorTtlWrapper实现了Executor接口,实现了方法execute,会自动帮我们包装Runnable对象。
public void execute(@NonNull Runnable command) {
executor.execute(TtlRunnable.get(command));
}
TtlRunnable的创建在父线程中,那么在构造的时候它会拿到父线程的所有TransmittableThreadLocal类型的变量,然后执行时会备份子线程的所有ThreadLocal变量,并且将父线程的TransmittableThreadLocal快照设置到当前线程中,当业务逻辑执行完之后,再恢复子线程的ThreadLocal变量。
这是快照父线程的TransmittableThreadLocal,以及用户自己注册的ThreadLocal。
private TtlRunnable(@NonNull Runnable runnable, boolean releaseTtlValueReferenceAfterRun) {
this.capturedRef = new AtomicReference<Object>(capture());
//...
}
这里是Runnable的run方法。
@Override
public void run() {
// 这里是拿到快照的值。
Object captured = capturedRef.get();
if (captured == null || releaseTtlValueReferenceAfterRun && !capturedRef.compareAndSet(captured, null)) {
throw new IllegalStateException("TTL value reference is released after run!");
}
//设置到当前线程中,即replay,backup是备份的子线程的ThreadLoal数据
Object backup = replay(captured);
try {
runnable.run();
} finally {
// 最后会恢复子线程的ThreadLocal数据
restore(backup);
}
}
因此通过组合使用TransmittableThreadLocal和TtlExecutors#getTtlExecutorService可以在线程池线程中拿到调用线程的TransmittableThreadLocal变量,并且在线程池线程执行完毕后清除掉调用线程的TransmittableThreadLocal变量,从而不会污染线程池线程。
解决方案二
如果不需要向线程池线程传递ThreadLocal变量,那么直接使用普通的ThreadLocal即可,而不是TransmittableThreadLocal,需要传递值就手动传递进去。