問題
在spring中使用@async異步調(diào)用的情況下,被調(diào)用的異步子線程獲取不到父線程的request信息廊营,以便處理相關(guān)邏輯歪泳,即子線程無法獲取父線程的上下文數(shù)據(jù)
思路
在自定義的異步線程池ThreadPoolTaskExecutor中,初始化線程池時有taskDecorator這樣一個任務(wù)裝飾器露筒,類似aop呐伞,可對線程執(zhí)行方法的始末進(jìn)行增強。其初始化源碼如下
protected ExecutorService initializeExecutor(ThreadFactory threadFactory, RejectedExecutionHandler rejectedExecutionHandler) {
BlockingQueue<Runnable> queue = this.createQueue(this.queueCapacity);
ThreadPoolExecutor executor;
if (this.taskDecorator != null) {
executor = new ThreadPoolExecutor(this.corePoolSize, this.maxPoolSize, (long)this.keepAliveSeconds, TimeUnit.SECONDS, queue, threadFactory, rejectedExecutionHandler) {
public void execute(Runnable command) {
Runnable decorated = ThreadPoolTaskExecutor.this.taskDecorator.decorate(command);
if (decorated != command) {
ThreadPoolTaskExecutor.this.decoratedTaskMap.put(decorated, command);
}
super.execute(decorated);
}
};
} else {
executor = new ThreadPoolExecutor(this.corePoolSize, this.maxPoolSize, (long)this.keepAliveSeconds, TimeUnit.SECONDS, queue, threadFactory, rejectedExecutionHandler);
}
if (this.allowCoreThreadTimeOut) {
executor.allowCoreThreadTimeOut(true);
}
this.threadPoolExecutor = executor;
return executor;
}
基本使用慎式,自定義裝飾器實現(xiàn)TaskDecorator 伶氢,重寫decorate方法,自定義線程池瘪吏,并設(shè)置自定義裝飾器
自定義異步線程池
@Bean("taskExecutor") // bean 的名稱癣防,默認(rèn)為首字母小寫的方法名
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
//其他參數(shù)省略
//設(shè)置裝飾器
threadPoolTaskExecutor.setTaskDecorator(new ContextCopyingDecorator());
return executor;
}
自定義裝飾器,ContextCopyingDecorator掌眠,通過try蕾盯,finally,在子線程執(zhí)行完后將該線程設(shè)置的上下文變量清除
public class ContextCopyingDecorator implements TaskDecorator {
@Override
public Runnable decorate(Runnable runnable) {
try {
//獲取父線程的context
RequestAttributes context = RequestContextHolder.currentRequestAttributes();
return () -> {
try {
//將父線程的context設(shè)置進(jìn)子線程里
RequestContextHolder.setRequestAttributes(context);
//子線程方法執(zhí)行
runnable.run();
} finally {
//清除子線程context
RequestContextHolder.resetRequestAttributes();
}
};
} catch (IllegalStateException e) {
return runnable;
}
}
}
存在問題
從父線程取出的RequestContextHolder對象蓝丙,此為持有線程上下文的request容器级遭,將其設(shè)置到子線程中,按道理只要對象還存在強引用渺尘,就不會被銷毀挫鸽,但由于RequestContextHolder的特殊性,在父線程銷毀的時候沧烈,會觸發(fā)里面的resetRequestAttributes方法(即清除threadLocal里面的信息掠兄,即reques中的信息會被清除),此時即使RequestContextHolder這個對象還是存在锌雀,子線程也無法繼續(xù)使用它獲取request中的數(shù)據(jù)了蚂夕。這也是網(wǎng)上很多文章講TaskDecorator時沒提到的點,真正用起來會發(fā)現(xiàn)有時可以有時不行腋逆,這個就取決于父子線程哪個先結(jié)束了婿牍。
完善思路
既然是RequestContextHolder的特殊性,那我們就讓繞過他的銷毀清除惩歉,思路不變等脂,還是繼續(xù)使用threadLocal來傳遞我們需要使用到的變量俏蛮,在父線程裝飾前將所需變量取出來,然后在子線程中設(shè)置到threadLocal上遥,業(yè)務(wù)使用的時候從threadLocal中取即可搏屑。
改造,自定義threadLocal類(此例子以ua為例子)粉楚,修改自定義裝飾器邏輯
public class ContextCopyingDecorator implements TaskDecorator {
@Override
public Runnable decorate(Runnable runnable) {
try {
//獲取父線程的request的user-agent(示例)
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
String ua = request.getHeader("user-agent");
return () -> {
try {
//將父線程的ua設(shè)置進(jìn)子線程里
ThreadLocalData.setUa(ua);
//子線程方法執(zhí)行
runnable.run();
} finally {
//清除線程threadLocal的值
ThreadLocalData.remove();
}
};
} catch (IllegalStateException e) {
return runnable;
}
}
}
ThreadLocalData
public class ThreadLocalData {
public static final ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static String getUa(){
return threadLocal.get();
}
public static void setUa(String ua){
threadLocal.set(ua);
}
public static void remove(){
threadLocal.remove();
}
}
至此經(jīng)測試辣恋,一切符合預(yù)期
涉及知識點
ThreadLocal,InheritableThreadLocal模软,TaskDecorator伟骨,RequestContextHolder,TransmittableThreadLocal(通過繼承InheritableThreadLocal實現(xiàn)燃异,阿里的携狭,推薦)
測試 ThreadLocal,InheritableThreadLocal回俐,TransmittableThreadLocal的區(qū)別和使用
1.父線程使用ThreadLocal逛腿,子線程創(chuàng)建時不會擁有父類的threadLocal信息
2.父線程使用InheritableThreadLocal,子線程創(chuàng)建時鲫剿,默認(rèn)init方法會拿到父類的InheritableThreadLocal信息鳄逾,這種在線程池/線程復(fù)用的情況下,由于init方法只會在初始化時獲取父線程的數(shù)據(jù)灵莲,復(fù)用的時候也沒法再從父線程那里新的InheritableThreadLocal的數(shù)據(jù)雕凹,此種情況下繼續(xù)使用,很容易出bug(InheritableThreadLocal適用于非線程池和復(fù)用線程政冻,單獨創(chuàng)建銷毀子線程執(zhí)行的情況)
3.父線程使用TransmittableThreadLocal枚抵,子線程創(chuàng)建時擁有父類的TransmittableThreadLocal信息,在線程池/線程復(fù)用的情況下不會出現(xiàn)讀取到臟數(shù)據(jù)的情況
總結(jié)
- 在異步線程池的情況下明场,通過ThreadLocal+TaskDecorator一般即可解決遇到的透傳問題(方式1)
- 使用阿里的TransmittableThreadLocal汽摹,其原理也是對Runnable,Callable苦锨,進(jìn)行裝飾(方式2)
參考
Spring線程池—TaskDecorator線程的裝飾(跨線程傳遞ThreadLocal的方案)
(28條消息) TaskDecorator——異步多線程中傳遞上下文等變量_WannaRunning的博客-CSDN博客