
InheritableThreadLocal簡介
上一篇文章我們聊到了ThreadLocal的作用機(jī)理,但是在文章的末尾,我提到了一個問題,ThreadLocal無法實(shí)現(xiàn)異步線程變量的傳遞。
什么意思呢?以下面的代碼為例子:
@SneakyThrows
public Boolean testThreadLocal(String s){
LOGGER.info("實(shí)際傳入的值為: " + s);
DemoContext.setContext(Integer.valueOf(s)); // DemoContext為相應(yīng)的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;
}
原本我們期待的結(jié)果是,子線程中的值與主線程中的值保持一致,但是實(shí)際上,運(yùn)行代碼返回的結(jié)果是:

由此可見,ThreadLocal并沒有按照所想的那樣將相應(yīng)的ThreadLocal的值傳遞到相應(yīng)的異步線程上。
為了實(shí)現(xiàn)異步線程變量的傳遞,InheritableThreadLocal應(yīng)運(yùn)而生(以下簡稱為:ITL)。
我們將上述的代碼稍作改動,將demoContext的類型轉(zhuǎn)換成ITL之后再運(yùn)行一次代碼??梢钥吹浇Y(jié)果如下:

ITL雖然傳遞了主線程的變量信息,但是在特定場景下也會出現(xiàn)問題。例如在上面的代碼中,如果我們設(shè)置相應(yīng)的線程池再來請求的話,就會出現(xiàn)問題。源碼如下:
@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); // 設(shè)置了線程池
...
}
@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;
}
代碼運(yùn)行起來的結(jié)果如下:

可以看到,多次請求后,線程間的變量出現(xiàn)了混亂傳遞,即實(shí)際傳入的值,與子線程中拿到的值并不一樣。這又是什么原因呢?

底層原理分析
要了解這個問題的原因,我們不得不了解下ITL的工作機(jī)制。
其實(shí)看起來,但是其實(shí)ITL的工作機(jī)制很簡單,就是在子線程初始化的時候,將父線程的ITL給繼承過來。具體來看Thread類中相應(yīng)的init源碼:
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
Thread parent = currentThread(); // 先找到他的爸爸
this.daemon = parent.isDaemon(); //判斷是否需要創(chuàng)建的是daemon線程
this.priority = parent.getPriority(); // 保持跟他爸一樣的優(yōu)先級
if (security == null || isCCLOverridden(parent.getClass())) // 獲取相應(yīng)的加載器
this.contextClassLoader = parent.getContextClassLoader();
else
this.contextClassLoader = parent.contextClassLoader;
this.inheritedAccessControlContext =
acc != null ? acc : AccessController.getContext();
this.target = target;
setPriority(priority);
/*關(guān)鍵性代碼*/
// 在這里會判斷當(dāng)前的是否需要inheritThreadLocals
//如果需要,那么會將當(dāng)前創(chuàng)建這個線程的InheritableThreadLocals都獲取過來,相當(dāng)于獲取了一份父類表的拷貝。
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
this.stackSize = stackSize;
tid = nextThreadID(); // 設(shè)置ThreadID
}
這種場景,在不使用線程池的情況是沒有問題的。但是如果搭配上了線程池,就會存在問題。這里我們先簡單介紹一下線程池的作用機(jī)理。

其中最關(guān)鍵的點(diǎn)在于,線程池會復(fù)用原有線程,致使部分線程不會經(jīng)過Init初始化的過程,ITL的值也就沒有辦法得到更新。最終造成了錯誤的數(shù)據(jù)傳遞。
優(yōu)劣勢分析
優(yōu)勢:
1、通過在線程初始化的時候傳遞相應(yīng)的ThreadLocal變量,解決了非線程池下的異步線程的變量傳遞問題。
劣勢:
1、線程池復(fù)用線程和ITL底層機(jī)制無法兼容,導(dǎo)致了ITL無法結(jié)合線程池發(fā)揮作用。
總結(jié):
在不依賴于線程池的場景下,ITL是一個很好的實(shí)現(xiàn)異步線程傳遞變量的工具。
然而,在使用線程池的情況下,由于線程不會進(jìn)行頻繁地初始化和銷毀等工作,ITL的變量值無法得到更新,因而有可能存在數(shù)據(jù)錯誤傳遞的問題。