Spring Boot中有多個(gè)@Async異步任務(wù)時(shí)蒜危,記得做好線程池的隔離!

通過(guò)上一篇:配置@Async異步任務(wù)的線程池的介紹睹耐,你應(yīng)該已經(jīng)了解到異步任務(wù)的執(zhí)行背后有一個(gè)線程池來(lái)管理執(zhí)行任務(wù)辐赞。為了控制異步任務(wù)的并發(fā)不影響到應(yīng)用的正常運(yùn)作,我們必須要對(duì)線程池做好相應(yīng)的配置疏橄,防止資源的過(guò)渡使用占拍。除了默認(rèn)線程池的配置之外略就,還有一類場(chǎng)景,也是很常見(jiàn)的晃酒,那就是多任務(wù)情況下的線程池隔離表牢。

什么是線程池的隔離,為什么要隔離

可能有的小伙伴還不太了解什么是線程池的隔離贝次,為什么要隔離崔兴?。所以蛔翅,我們先來(lái)看看下面的場(chǎng)景案例:

@RestController
public class HelloController {

    @Autowired
    private AsyncTasks asyncTasks;
        
    @GetMapping("/api-1")
    public String taskOne() {
        CompletableFuture<String> task1 = asyncTasks.doTaskOne("1");
        CompletableFuture<String> task2 = asyncTasks.doTaskOne("2");
        CompletableFuture<String> task3 = asyncTasks.doTaskOne("3");
        
        CompletableFuture.allOf(task1, task2, task3).join();
        return "";
    }
    
    @GetMapping("/api-2")
    public String taskTwo() {
        CompletableFuture<String> task1 = asyncTasks.doTaskTwo("1");
        CompletableFuture<String> task2 = asyncTasks.doTaskTwo("2");
        CompletableFuture<String> task3 = asyncTasks.doTaskTwo("3");
        
        CompletableFuture.allOf(task1, task2, task3).join();
        return "";
    }
    
}

上面的代碼中敲茄,有兩個(gè)API接口,這兩個(gè)接口的具體執(zhí)行邏輯中都會(huì)把執(zhí)行過(guò)程拆分為三個(gè)異步任務(wù)來(lái)實(shí)現(xiàn)山析。

好了堰燎,思考一分鐘,想一下笋轨。如果這樣實(shí)現(xiàn)秆剪,會(huì)有什么問(wèn)題嗎?


上面這段代碼爵政,在API請(qǐng)求并發(fā)不高仅讽,同時(shí)如果每個(gè)任務(wù)的處理速度也夠快的時(shí)候,是沒(méi)有問(wèn)題的钾挟。但如果并發(fā)上來(lái)或其中某幾個(gè)處理過(guò)程扯后腿了的時(shí)候洁灵。這兩個(gè)提供不相干服務(wù)的接口可能會(huì)互相影響。比如:假設(shè)當(dāng)前線程池配置的最大線程數(shù)有2個(gè)掺出,這個(gè)時(shí)候/api-1接口中task1和task2處理速度很慢徽千,阻塞了;那么此時(shí)蛛砰,當(dāng)用戶調(diào)用api-2接口的時(shí)候罐栈,這個(gè)服務(wù)也會(huì)阻塞!

造成這種現(xiàn)場(chǎng)的原因是:默認(rèn)情況下泥畅,所有用@Async創(chuàng)建的異步任務(wù)都是共用的一個(gè)線程池荠诬,所以當(dāng)有一些異步任務(wù)碰到性能問(wèn)題的時(shí)候,是會(huì)直接影響其他異步任務(wù)的位仁。

為了解決這個(gè)問(wèn)題柑贞,我們就需要對(duì)異步任務(wù)做一定的線程池隔離,讓不同的異步任務(wù)互不影響聂抢。

不同異步任務(wù)配置不同線程池

下面钧嘶,我們就來(lái)實(shí)際操作一下!

第一步:初始化多個(gè)線程池琳疏,比如下面這樣:

@EnableAsync
@Configuration
public class TaskPoolConfig {

    @Bean
    public Executor taskExecutor1() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(2);
        executor.setMaxPoolSize(2);
        executor.setQueueCapacity(10);
        executor.setKeepAliveSeconds(60);
        executor.setThreadNamePrefix("executor-1-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        return executor;
    }

    @Bean
    public Executor taskExecutor2() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(2);
        executor.setMaxPoolSize(2);
        executor.setQueueCapacity(10);
        executor.setKeepAliveSeconds(60);
        executor.setThreadNamePrefix("executor-2-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        return executor;
    }
}

注意:這里特地用executor.setThreadNamePrefix設(shè)置了線程名的前綴有决,這樣可以方便觀察后面具體執(zhí)行的順序闸拿。

第二步:創(chuàng)建異步任務(wù),并指定要使用的線程池名稱

@Slf4j
@Component
public class AsyncTasks {

    public static Random random = new Random();

    @Async("taskExecutor1")
    public CompletableFuture<String> doTaskOne(String taskNo) throws Exception {
        log.info("開(kāi)始任務(wù):{}", taskNo);
        long start = System.currentTimeMillis();
        Thread.sleep(random.nextInt(10000));
        long end = System.currentTimeMillis();
        log.info("完成任務(wù):{}书幕,耗時(shí):{} 毫秒", taskNo, end - start);
        return CompletableFuture.completedFuture("任務(wù)完成");
    }

    @Async("taskExecutor2")
    public CompletableFuture<String> doTaskTwo(String taskNo) throws Exception {
        log.info("開(kāi)始任務(wù):{}", taskNo);
        long start = System.currentTimeMillis();
        Thread.sleep(random.nextInt(10000));
        long end = System.currentTimeMillis();
        log.info("完成任務(wù):{}新荤,耗時(shí):{} 毫秒", taskNo, end - start);
        return CompletableFuture.completedFuture("任務(wù)完成");
    }

}

這里@Async注解中定義的taskExecutor1taskExecutor2就是線程池的名字。由于在第一步中台汇,我們沒(méi)有具體寫(xiě)兩個(gè)線程池Bean的名稱苛骨,所以默認(rèn)會(huì)使用方法名,也就是taskExecutor1taskExecutor2苟呐。

第三步:寫(xiě)個(gè)單元測(cè)試來(lái)驗(yàn)證下痒芝,比如下面這樣:

@Slf4j
@SpringBootTest
public class Chapter77ApplicationTests {

    @Autowired
    private AsyncTasks asyncTasks;

    @Test
    public void test() throws Exception {
        long start = System.currentTimeMillis();

        // 線程池1
        CompletableFuture<String> task1 = asyncTasks.doTaskOne("1");
        CompletableFuture<String> task2 = asyncTasks.doTaskOne("2");
        CompletableFuture<String> task3 = asyncTasks.doTaskOne("3");

        // 線程池2
        CompletableFuture<String> task4 = asyncTasks.doTaskTwo("4");
        CompletableFuture<String> task5 = asyncTasks.doTaskTwo("5");
        CompletableFuture<String> task6 = asyncTasks.doTaskTwo("6");

        // 一起執(zhí)行
        CompletableFuture.allOf(task1, task2, task3, task4, task5, task6).join();

        long end = System.currentTimeMillis();

        log.info("任務(wù)全部完成,總耗時(shí):" + (end - start) + "毫秒");
    }

}

在上面的單元測(cè)試中牵素,一共啟動(dòng)了6個(gè)異步任務(wù)严衬,前三個(gè)用的是線程池1,后三個(gè)用的是線程池2两波。

先不執(zhí)行瞳步,根據(jù)設(shè)置的核心線程2和最大線程數(shù)2闷哆,來(lái)分析一下腰奋,大概會(huì)是怎么樣的執(zhí)行情況?

  1. 線程池1的三個(gè)任務(wù)抱怔,task1和task2會(huì)先獲得執(zhí)行線程劣坊,然后task3因?yàn)闆](méi)有可分配線程進(jìn)入緩沖隊(duì)列
  2. 線程池2的三個(gè)任務(wù),task4和task5會(huì)先獲得執(zhí)行線程屈留,然后task6因?yàn)闆](méi)有可分配線程進(jìn)入緩沖隊(duì)列
  3. 任務(wù)task3會(huì)在task1或task2完成之后局冰,開(kāi)始執(zhí)行
  4. 任務(wù)task6會(huì)在task4或task5完成之后,開(kāi)始執(zhí)行

分析好之后灌危,執(zhí)行下單元測(cè)試康二,看看是否是這樣的:

2021-09-15 23:45:11.369  INFO 61670 --- [   executor-1-1] com.didispace.chapter77.AsyncTasks       : 開(kāi)始任務(wù):1
2021-09-15 23:45:11.369  INFO 61670 --- [   executor-2-2] com.didispace.chapter77.AsyncTasks       : 開(kāi)始任務(wù):5
2021-09-15 23:45:11.369  INFO 61670 --- [   executor-2-1] com.didispace.chapter77.AsyncTasks       : 開(kāi)始任務(wù):4
2021-09-15 23:45:11.369  INFO 61670 --- [   executor-1-2] com.didispace.chapter77.AsyncTasks       : 開(kāi)始任務(wù):2
2021-09-15 23:45:15.905  INFO 61670 --- [   executor-2-1] com.didispace.chapter77.AsyncTasks       : 完成任務(wù):4,耗時(shí):4532 毫秒
2021-09-15 23:45:15.905  INFO 61670 --- [   executor-2-1] com.didispace.chapter77.AsyncTasks       : 開(kāi)始任務(wù):6
2021-09-15 23:45:18.263  INFO 61670 --- [   executor-1-2] com.didispace.chapter77.AsyncTasks       : 完成任務(wù):2勇蝙,耗時(shí):6890 毫秒
2021-09-15 23:45:18.263  INFO 61670 --- [   executor-1-2] com.didispace.chapter77.AsyncTasks       : 開(kāi)始任務(wù):3
2021-09-15 23:45:18.896  INFO 61670 --- [   executor-2-2] com.didispace.chapter77.AsyncTasks       : 完成任務(wù):5沫勿,耗時(shí):7523 毫秒
2021-09-15 23:45:19.842  INFO 61670 --- [   executor-1-2] com.didispace.chapter77.AsyncTasks       : 完成任務(wù):3,耗時(shí):1579 毫秒
2021-09-15 23:45:20.551  INFO 61670 --- [   executor-1-1] com.didispace.chapter77.AsyncTasks       : 完成任務(wù):1味混,耗時(shí):9178 毫秒
2021-09-15 23:45:24.117  INFO 61670 --- [   executor-2-1] com.didispace.chapter77.AsyncTasks       : 完成任務(wù):6产雹,耗時(shí):8212 毫秒
2021-09-15 23:45:24.117  INFO 61670 --- [           main] c.d.chapter77.Chapter77ApplicationTests  : 任務(wù)全部完成,總耗時(shí):12762毫秒

好了翁锡,今天的學(xué)習(xí)就到這里蔓挖!如果您學(xué)習(xí)過(guò)程中如遇困難?可以加入我們超高質(zhì)量的Spring技術(shù)交流群馆衔,參與交流與討論瘟判,更好的學(xué)習(xí)與進(jìn)步怨绣!更多Spring Boot教程可以點(diǎn)擊直達(dá)!拷获,歡迎收藏與轉(zhuǎn)發(fā)支持梨熙!

代碼示例

本文的完整工程可以查看下面?zhèn)}庫(kù)中2.x目錄下的chapter7-7工程:

如果您覺(jué)得本文不錯(cuò),歡迎Star支持刀诬,您的關(guān)注是我堅(jiān)持的動(dòng)力咽扇!

歡迎關(guān)注我的公眾號(hào):程序猿DD,分享外面看不到的干貨陕壹!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末质欲,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子糠馆,更是在濱河造成了極大的恐慌嘶伟,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,657評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件又碌,死亡現(xiàn)場(chǎng)離奇詭異九昧,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)毕匀,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,889評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門(mén)铸鹰,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人皂岔,你說(shuō)我怎么就攤上這事蹋笼。” “怎么了躁垛?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,057評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵剖毯,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我教馆,道長(zhǎng)逊谋,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,509評(píng)論 1 293
  • 正文 為了忘掉前任土铺,我火速辦了婚禮胶滋,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘舒憾。我一直安慰自己镀钓,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,562評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布镀迂。 她就那樣靜靜地躺著丁溅,像睡著了一般。 火紅的嫁衣襯著肌膚如雪探遵。 梳的紋絲不亂的頭發(fā)上窟赏,一...
    開(kāi)封第一講書(shū)人閱讀 51,443評(píng)論 1 302
  • 那天妓柜,我揣著相機(jī)與錄音,去河邊找鬼涯穷。 笑死棍掐,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的拷况。 我是一名探鬼主播作煌,決...
    沈念sama閱讀 40,251評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼赚瘦!你這毒婦竟也來(lái)了粟誓?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,129評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤起意,失蹤者是張志新(化名)和其女友劉穎鹰服,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體揽咕,經(jīng)...
    沈念sama閱讀 45,561評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡悲酷,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,779評(píng)論 3 335
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了亲善。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片设易。...
    茶點(diǎn)故事閱讀 39,902評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖逗爹,靈堂內(nèi)的尸體忽然破棺而出亡嫌,到底是詐尸還是另有隱情,我是刑警寧澤掘而,帶...
    沈念sama閱讀 35,621評(píng)論 5 345
  • 正文 年R本政府宣布,位于F島的核電站于购,受9級(jí)特大地震影響袍睡,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜肋僧,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,220評(píng)論 3 328
  • 文/蒙蒙 一斑胜、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧嫌吠,春花似錦止潘、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,838評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至炕矮,卻和暖如春么夫,著一層夾襖步出監(jiān)牢的瞬間者冤,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,971評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工档痪, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留涉枫,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,025評(píng)論 2 370
  • 正文 我出身青樓腐螟,卻偏偏與公主長(zhǎng)得像愿汰,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子乐纸,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,843評(píng)論 2 354

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