問題復(fù)現(xiàn):
項(xiàng)目?jī)?nèi)原本采用的是DemoContext作為一個(gè)線程的上下文context,用于存儲(chǔ)從header頭、入?yún)?shù)的一部分?jǐn)?shù)據(jù),實(shí)現(xiàn)跨業(yè)務(wù)代碼復(fù)用及傳遞。
public class DemoContext {
...
//創(chuàng)建一個(gè)ThreadLocal
private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
...
}
@SneakyThrows
public Boolean testThreadLocal(String s){
LOGGER.info("實(shí)際傳入的值為"+s);
//設(shè)置對(duì)應(yīng)傳入的值
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;
}
但是實(shí)際ThreadLocal本身,是針對(duì)每個(gè)線程實(shí)現(xiàn)單獨(dú)數(shù)據(jù)存儲(chǔ)的,并沒有實(shí)現(xiàn)線程變量的傳遞,因而導(dǎo)致子線程無法獲取到父線程的變量參數(shù),從而導(dǎo)致業(yè)務(wù)邏輯代碼本身出錯(cuò)。
2022-01-14 16:21:53.565 INFO 97654 --- [nio-8080-exec-2] c.example.demo.service.aop.TestService : 實(shí)際傳入的值為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為:
改進(jìn)一:InheritableThreadLocal
翻閱了網(wǎng)上的資料,了解到目前能夠?qū)崿F(xiàn)線程變量傳遞的方式主要是(ITL)和(TTL)兩種方式,因而嘗試性的使用了第一種方法,即采用ITL的方式實(shí)現(xiàn)。
代碼改動(dòng)主要如下:
public class DemoContext {
...
//創(chuàng)建一個(gè)ThreadLocal
private static final ThreadLocal<String> CONTEXT_HOLDER = new InheritableThreadLocal<>();
...
}
經(jīng)嘗試,父子線程確實(shí)已經(jīng)可以傳遞變量了,一下子安然自得不少~。同參數(shù)請(qǐng)求結(jié)果如下:
2022-01-14 16:40:30.476 INFO 98846 --- [nio-8080-exec-8] c.example.demo.service.aop.TestService : 實(shí)際傳入的值為: 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 : 實(shí)際傳入的值為: 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
...
但是過了一陣時(shí)間后,發(fā)現(xiàn)出現(xiàn)了新的問題,子線程內(nèi)攜帶的變量和主線程實(shí)際變量不一致,造成了業(yè)務(wù)數(shù)據(jù)查詢混亂的問題。
2022-01-14 16:41:18.449 INFO 98846 --- [nio-8080-exec-1] c.example.demo.service.aop.TestService : 實(shí)際傳入的值為: 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
搜尋了相關(guān)文章內(nèi)容研究發(fā)現(xiàn),InheritableThreadLocal的原理是在子線程初始化的時(shí)候,將父線程的InheritableThreadLocal拷貝到子線程內(nèi)。具體源碼如下:
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);
...
}
但是問題在于,絕大多數(shù)的項(xiàng)目中會(huì)用到線程池,而線程池的工作機(jī)制就是【將當(dāng)前工作的線程再次復(fù)用】,因此,線程池是不會(huì)進(jìn)行線程初始化的調(diào)用的。也就導(dǎo)致了單純使用InheritThreadLocal會(huì)出現(xiàn)數(shù)據(jù)污染的問題。
改進(jìn)二:TransmittableThreadLocal
針對(duì)該問題,阿里的大佬們自行研發(fā)和開源了相應(yīng)的組件TransmittableThreadLocal解決了這一痛點(diǎn)。
TransmittableThreadLocal繼承了InheritThreadLocal類并對(duì)其進(jìn)行的增強(qiáng)。
其使用主要有以下幾種:
一、針對(duì)普通task執(zhí)行的方式:
@SneakyThrows
public Boolean testNormalThreadTask(String s){
LOGGER.info("實(shí)際傳入的值為: " + s);
//設(shè)置對(duì)應(yīng)傳入的值
DemoContext.setContext(Integer.valueOf(s));
Runnable runnable = () -> LOGGER.info(String.format("子線程id=%s,contextStr為:%s", Thread.currentThread().getId(), DemoContext.getContext()));
//關(guān)鍵性代碼,采用TtlRunnable進(jìn)行裝飾
Runnable ttlRunnable = TtlRunnable.get(runnable);
demoExecutor.submit(ttlRunnable);
LOGGER.info(String.format("主線程id=%s,contextStr為:%s",Thread.currentThread().getId(),DemoContext.getContext()));
return true;
}
二、針對(duì)線程池的執(zhí)行方式:
針對(duì)線程池,自然也是可以先修飾task,再調(diào)用線程池執(zhí)行的方式。亦或者是通過對(duì)線程池進(jìn)行包裝,從而獲取新的線程池變量。主要支持的包裝方法有以下幾個(gè):
省去每次Runnable
和Callable
傳入線程池時(shí)的修飾,這個(gè)邏輯可以在線程池中完成。
通過工具類com.alibaba.ttl.threadpool.TtlExecutors
完成,有下面的方法:
-
getTtlExecutor
:修飾接口Executor
-
getTtlExecutorService
:修飾接口ExecutorService
-
getTtlScheduledExecutorService
:修飾接口ScheduledExecutorService
這里我以getTtlExecutor為例子,將對(duì)應(yīng)的線程池進(jìn)行包裝后,發(fā)現(xiàn)問題得到解決。
@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();
//對(duì)相應(yīng)的線程池進(jìn)行包裝
return TtlExecutors.getTtlExecutor(threadPoolTaskExecutor.getThreadPoolExecutor());
}
三、針對(duì)java代碼還有無侵入方式的解決方案
即借助于javaAgent實(shí)現(xiàn)的代理方式,這種方式能夠?qū)Υa實(shí)現(xiàn)無侵入。
通過設(shè)置一個(gè)ThreadLocalAgent,來達(dá)到目的。
@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
更詳細(xì)的步驟可以參考transmittable-thread-local
改進(jìn)三:自定義裝飾器
ThreadPoolTaskExecutor本身也是支持設(shè)置對(duì)應(yīng)的裝飾器的,因此,我們也可以對(duì)裝飾器進(jìn)行重載,在子線程進(jìn)行runnable任務(wù)的時(shí)候,將父線程的Context變量傳入到子線程的Context變量中,從而實(shí)現(xiàn)對(duì)應(yīng)的變量傳遞。
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 : 實(shí)際傳入的值為: 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 : 實(shí)際傳入的值為: 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