问题复现:
项目内原本采用的是DemoContext作为一个线程的上下文context,用于存储从header头、入参数的一部分数据,实现跨业务代码复用及传递。
public class DemoContext {
...
//创建一个ThreadLocal
private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
...
}
@SneakyThrows
public Boolean testThreadLocal(String s){
LOGGER.info("实际传入的值为"+s);
//设置对应传入的值
DemoContext.setContext(s);
CompletableFuture<Throwable> subThread = CompletableFuture.supplyAsync(()->{
try{
//打印子线程的值
LOGGER.info("子线程的contextStr为:" + DemoContext.getContext());
}catch (Throwable throwable){
return throwable;
}
return null;
});
//打印主线程的值
LOGGER.info("主线程的contextStr为:" + DemoContext.getContext());
Throwable throwable = subThread.get();
if (throwable!=null){
throw throwable;
}
DemoContext.clearContext();
return true;
}
但是实际ThreadLocal本身,是针对每个线程实现单独数据存储的,并没有实现线程变量的传递,因而导致子线程无法获取到父线程的变量参数,从而导致业务逻辑代码本身出错。
2022-01-14 16:21:53.565 INFO 97654 --- [nio-8080-exec-2] c.example.demo.service.aop.TestService : 实际传入的值为1
2022-01-14 16:21:55.331 INFO 97654 --- [nio-8080-exec-2] c.example.demo.service.aop.TestService : 主线程的contextStr为:1
2022-01-14 16:21:55.331 INFO 97654 --- [onPool-worker-1] c.example.demo.service.aop.TestService : 子线程的contextStr为:
改进一:InheritableThreadLocal
翻阅了网上的资料,了解到目前能够实现线程变量传递的方式主要是(ITL)和(TTL)两种方式,因而尝试性的使用了第一种方法,即采用ITL的方式实现。
代码改动主要如下:
public class DemoContext {
...
//创建一个ThreadLocal
private static final ThreadLocal<String> CONTEXT_HOLDER = new InheritableThreadLocal<>();
...
}
经尝试,父子线程确实已经可以传递变量了,一下子安然自得不少~。同参数请求结果如下:
2022-01-14 16:40:30.476 INFO 98846 --- [nio-8080-exec-8] c.example.demo.service.aop.TestService : 实际传入的值为: 1
2022-01-14 16:40:30.477 INFO 98846 --- [onPool-worker-5] c.example.demo.service.aop.TestService : 子线程id=51,contextStr为:1
2022-01-14 16:40:30.477 INFO 98846 --- [nio-8080-exec-8] c.example.demo.service.aop.TestService : 主线程id=46,contextStr为:1
2022-01-14 16:40:35.045 INFO 98846 --- [nio-8080-exec-9] c.example.demo.service.aop.TestService : 实际传入的值为: 1
2022-01-14 16:40:35.045 INFO 98846 --- [nio-8080-exec-9] c.example.demo.service.aop.TestService : 主线程id=48,contextStr为:1
2022-01-14 16:40:35.045 INFO 98846 --- [onPool-worker-5] c.example.demo.service.aop.TestService : 子线程id=51,contextStr为:1
...
但是过了一阵时间后,发现出现了新的问题,子线程内携带的变量和主线程实际变量不一致,造成了业务数据查询混乱的问题。
2022-01-14 16:41:18.449 INFO 98846 --- [nio-8080-exec-1] c.example.demo.service.aop.TestService : 实际传入的值为: 1
2022-01-14 16:41:18.449 INFO 98846 --- [nio-8080-exec-1] c.example.demo.service.aop.TestService : 主线程id=37,contextStr为:1
2022-01-14 16:41:18.449 INFO 98846 --- [onPool-worker-6] c.example.demo.service.aop.TestService : 子线程id=52,contextStr为:2
搜寻了相关文章内容研究发现,InheritableThreadLocal的原理是在子线程初始化的时候,将父线程的InheritableThreadLocal拷贝到子线程内。具体源码如下:
private void init(ThreadGroup g, Runnable target, String name,long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
...
//如果需要集成ThreadLocal 且父亲的InheritableThreadLocal不为空
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
...
}
但是问题在于,绝大多数的项目中会用到线程池,而线程池的工作机制就是【将当前工作的线程再次复用】,因此,线程池是不会进行线程初始化的调用的。也就导致了单纯使用InheritThreadLocal会出现数据污染的问题。
改进二:TransmittableThreadLocal
针对该问题,阿里的大佬们自行研发和开源了相应的组件TransmittableThreadLocal解决了这一痛点。
TransmittableThreadLocal继承了InheritThreadLocal类并对其进行的增强。
其使用主要有以下几种:
一、针对普通task执行的方式:
@SneakyThrows
public Boolean testNormalThreadTask(String s){
LOGGER.info("实际传入的值为: " + s);
//设置对应传入的值
DemoContext.setContext(Integer.valueOf(s));
Runnable runnable = () -> LOGGER.info(String.format("子线程id=%s,contextStr为:%s", Thread.currentThread().getId(), DemoContext.getContext()));
//关键性代码,采用TtlRunnable进行装饰
Runnable ttlRunnable = TtlRunnable.get(runnable);
demoExecutor.submit(ttlRunnable);
LOGGER.info(String.format("主线程id=%s,contextStr为:%s",Thread.currentThread().getId(),DemoContext.getContext()));
return true;
}
二、针对线程池的执行方式:
针对线程池,自然也是可以先修饰task,再调用线程池执行的方式。亦或者是通过对线程池进行包装,从而获取新的线程池变量。主要支持的包装方法有以下几个:
省去每次Runnable
和Callable
传入线程池时的修饰,这个逻辑可以在线程池中完成。
通过工具类com.alibaba.ttl.threadpool.TtlExecutors
完成,有下面的方法:
-
getTtlExecutor
:修饰接口Executor
-
getTtlExecutorService
:修饰接口ExecutorService
-
getTtlScheduledExecutorService
:修饰接口ScheduledExecutorService
这里我以getTtlExecutor为例子,将对应的线程池进行包装后,发现问题得到解决。
@Bean(name = "demoExecutor")
public Executor demoExecutor() {
ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
threadPoolTaskExecutor.setCorePoolSize(5);
threadPoolTaskExecutor.setQueueCapacity(0);
threadPoolTaskExecutor.setKeepAliveSeconds(3600);
threadPoolTaskExecutor.setMaxPoolSize(50);
threadPoolTaskExecutor.setThreadNamePrefix("demoExecutor-");
threadPoolTaskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
threadPoolTaskExecutor.setWaitForTasksToCompleteOnShutdown(true);
threadPoolTaskExecutor.initialize();
//对相应的线程池进行包装
return TtlExecutors.getTtlExecutor(threadPoolTaskExecutor.getThreadPoolExecutor());
}
三、针对java代码还有无侵入方式的解决方案
即借助于javaAgent实现的代理方式,这种方式能够对代码实现无侵入。
通过设置一个ThreadLocalAgent,来达到目的。
@Slf4j
public final class ThreadLocalAgent {
public static void premain(String agentArgs, Instrumentation inst) {
TtlAgent.premain(agentArgs, inst); // add TTL Transformer
}
}
注意,在bootclasspath
上,还是要加上TTL Jar
:
-Xbootclasspath/a:/path/to/transmittable-thread-local-2.x.y.jar:/path/to/your/agent/jar/files
更详细的步骤可以参考transmittable-thread-local
改进三:自定义装饰器
ThreadPoolTaskExecutor本身也是支持设置对应的装饰器的,因此,我们也可以对装饰器进行重载,在子线程进行runnable任务的时候,将父线程的Context变量传入到子线程的Context变量中,从而实现对应的变量传递。
public class GatewayHeaderTaskDecorator implements TaskDecorator {
@Override
public Runnable decorate(Runnable runnable) {
// 获取父线程的DemoContext
Integer contextInt = DemoContext.getContext();
return () -> {
try {
// 添加到子线程中 完成拷贝
DemoContext.setContext(contextInt);
runnable.run();
} finally {
DemoContext.clearContext();
}
};
}
}
2022-01-14 17:50:27.446 INFO 5969 --- [nio-8080-exec-2] c.example.demo.service.aop.TestService : 实际传入的值为: 2
2022-01-14 17:50:27.451 INFO 5969 --- [onPool-worker-2] c.example.demo.service.aop.TestService : 子线程id=64,contextStr为:2
2022-01-14 17:50:27.451 INFO 5969 --- [nio-8080-exec-2] c.example.demo.service.aop.TestService : 主线程id=63,contextStr为:2
2022-01-14 17:50:31.135 INFO 5969 --- [nio-8080-exec-3] c.example.demo.service.aop.TestService : 实际传入的值为: 2
2022-01-14 17:50:31.135 INFO 5969 --- [nio-8080-exec-3] c.example.demo.service.aop.TestService : 主线程id=65,contextStr为:2
2022-01-14 17:50:31.135 INFO 5969 --- [onPool-worker-2] c.example.demo.service.aop.TestService : 子线程id=64,contextStr为:2