异步线程变量传递必知必会---InheritableThreadLocal及底层原理分析
InheritableThreadLocal简介
上一篇文章我们聊到了ThreadLocal的作用机理,但是在文章的末尾,我提到了一个问题,ThreadLocal无法实现异步线程变量的传递。
什么意思呢?以下面的代码为例子:
@SneakyThrows
public Boolean testThreadLocal(String s){
LOGGER.info("实际传入的值为: " + s);
DemoContext.setContext(Integer.valueOf(s)); // DemoContext为相应的ThreadLocal对象
CompletableFuture<Throwable> subThread = CompletableFuture.supplyAsync(()->{
try{
//打印子线程的值
LOGGER.info(String.format("子线程id=%s,contextStr为:%s",
Thread.currentThread().getId(),DemoContext.getContext()));
}catch (Throwable throwable){
return throwable;
}
return null;
});
//打印主线程的值
LOGGER.info(String.format("主线程id=%s,contextStr为:%s",
Thread.currentThread().getId(),DemoContext.getContext()));
Throwable throwable = subThread.get();
if (throwable!=null){
throw throwable;
}
DemoContext.clearContext();
return true;
}
原本我们期待的结果是,子线程中的值与主线程中的值保持一致,但是实际上,运行代码返回的结果是:
由此可见,ThreadLocal并没有按照所想的那样将相应的ThreadLocal的值传递到相应的异步线程上。
为了实现异步线程变量的传递,InheritableThreadLocal应运而生(以下简称为:ITL)。
我们将上述的代码稍作改动,将demoContext的类型转换成ITL之后再运行一次代码。可以看到结果如下:
ITL虽然传递了主线程的变量信息,但是在特定场景下也会出现问题。例如在上面的代码中,如果我们设置相应的线程池再来请求的话,就会出现问题。源码如下:
@SneakyThrows
public Boolean testThreadLocal(String s){
...
CompletableFuture<Throwable> subThread = CompletableFuture.supplyAsync(()->{
try{
//打印子线程的值
LOGGER.info(String.format("子线程id=%s,contextStr为:%s",
Thread.currentThread().getId(),DemoContext.getContext()));
}catch (Throwable throwable){
return throwable;
}
return null;
},demoExecutor); // 设置了线程池
...
}
@Bean(name = "demoExecutor")
public Executor demoExecutor() {
ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
threadPoolTaskExecutor.setTaskDecorator(new GatewayHeaderTaskDecorator());
threadPoolTaskExecutor.setCorePoolSize(5);
threadPoolTaskExecutor.setQueueCapacity(0);
threadPoolTaskExecutor.setKeepAliveSeconds(3600);
threadPoolTaskExecutor.setMaxPoolSize(1);
threadPoolTaskExecutor.setThreadNamePrefix("demoExecutor-");
threadPoolTaskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
threadPoolTaskExecutor.setWaitForTasksToCompleteOnShutdown(true);
threadPoolTaskExecutor.initialize();
return threadPoolTaskExecutor;
}
代码运行起来的结果如下:
可以看到,多次请求后,线程间的变量出现了混乱传递,即实际传入的值,与子线程中拿到的值并不一样。这又是什么原因呢?
底层原理分析
要了解这个问题的原因,我们不得不了解下ITL的工作机制。
其实看起来,但是其实ITL的工作机制很简单,就是在子线程初始化的时候,将父线程的ITL给继承过来。具体来看Thread类中相应的init源码:
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
Thread parent = currentThread(); // 先找到他的爸爸
this.daemon = parent.isDaemon(); //判断是否需要创建的是daemon线程
this.priority = parent.getPriority(); // 保持跟他爸一样的优先级
if (security == null || isCCLOverridden(parent.getClass())) // 获取相应的加载器
this.contextClassLoader = parent.getContextClassLoader();
else
this.contextClassLoader = parent.contextClassLoader;
this.inheritedAccessControlContext =
acc != null ? acc : AccessController.getContext();
this.target = target;
setPriority(priority);
/*关键性代码*/
// 在这里会判断当前的是否需要inheritThreadLocals
//如果需要,那么会将当前创建这个线程的InheritableThreadLocals都获取过来,相当于获取了一份父类表的拷贝。
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
this.stackSize = stackSize;
tid = nextThreadID(); // 设置ThreadID
}
这种场景,在不使用线程池的情况是没有问题的。但是如果搭配上了线程池,就会存在问题。这里我们先简单介绍一下线程池的作用机理。
其中最关键的点在于,线程池会复用原有线程,致使部分线程不会经过Init初始化的过程,ITL的值也就没有办法得到更新。最终造成了错误的数据传递。
优劣势分析
优势:
1、通过在线程初始化的时候传递相应的ThreadLocal变量,解决了非线程池下的异步线程的变量传递问题。
劣势:
1、线程池复用线程和ITL底层机制无法兼容,导致了ITL无法结合线程池发挥作用。
总结:
在不依赖于线程池的场景下,ITL是一个很好的实现异步线程传递变量的工具。
然而,在使用线程池的情况下,由于线程不会进行频繁地初始化和销毁等工作,ITL的变量值无法得到更新,因而有可能存在数据错误传递的问题。
转自:https://zhuanlan.zhihu.com/p/469859090