Spring異步線程池—傳遞線程上下文(TaskDecorator實現(xiàn))

問題

在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博客

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末逼泣,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子舟舒,更是在濱河造成了極大的恐慌拉庶,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,817評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件秃励,死亡現(xiàn)場離奇詭異氏仗,居然都是意外死亡,警方通過查閱死者的電腦和手機夺鲜,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,329評論 3 385
  • 文/潘曉璐 我一進(jìn)店門皆尔,熙熙樓的掌柜王于貴愁眉苦臉地迎上來呐舔,“玉大人,你說我怎么就攤上這事慷蠕∩浩矗” “怎么了?”我有些...
    開封第一講書人閱讀 157,354評論 0 348
  • 文/不壞的土叔 我叫張陵砌们,是天一觀的道長杆麸。 經(jīng)常有香客問我,道長浪感,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,498評論 1 284
  • 正文 為了忘掉前任饼问,我火速辦了婚禮影兽,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘莱革。我一直安慰自己峻堰,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 65,600評論 6 386
  • 文/花漫 我一把揭開白布盅视。 她就那樣靜靜地躺著捐名,像睡著了一般。 火紅的嫁衣襯著肌膚如雪闹击。 梳的紋絲不亂的頭發(fā)上镶蹋,一...
    開封第一講書人閱讀 49,829評論 1 290
  • 那天,我揣著相機與錄音赏半,去河邊找鬼贺归。 笑死,一個胖子當(dāng)著我的面吹牛断箫,可吹牛的內(nèi)容都是我干的拂酣。 我是一名探鬼主播,決...
    沈念sama閱讀 38,979評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼仲义,長吁一口氣:“原來是場噩夢啊……” “哼婶熬!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起埃撵,我...
    開封第一講書人閱讀 37,722評論 0 266
  • 序言:老撾萬榮一對情侶失蹤赵颅,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后盯另,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體性含,經(jīng)...
    沈念sama閱讀 44,189評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,519評論 2 327
  • 正文 我和宋清朗相戀三年鸳惯,在試婚紗的時候發(fā)現(xiàn)自己被綠了商蕴。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片叠萍。...
    茶點故事閱讀 38,654評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖绪商,靈堂內(nèi)的尸體忽然破棺而出苛谷,到底是詐尸還是另有隱情,我是刑警寧澤格郁,帶...
    沈念sama閱讀 34,329評論 4 330
  • 正文 年R本政府宣布腹殿,位于F島的核電站,受9級特大地震影響例书,放射性物質(zhì)發(fā)生泄漏锣尉。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,940評論 3 313
  • 文/蒙蒙 一决采、第九天 我趴在偏房一處隱蔽的房頂上張望自沧。 院中可真熱鬧,春花似錦树瞭、人聲如沸拇厢。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,762評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽孝偎。三九已至,卻和暖如春凉敲,著一層夾襖步出監(jiān)牢的瞬間衣盾,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,993評論 1 266
  • 我被黑心中介騙來泰國打工荡陷, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留雨效,地道東北人。 一個月前我還...
    沈念sama閱讀 46,382評論 2 360
  • 正文 我出身青樓废赞,卻偏偏與公主長得像徽龟,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子唉地,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,543評論 2 349

推薦閱讀更多精彩內(nèi)容