溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

在Spring環(huán)境中怎么正確關(guān)閉線程池

發(fā)布時(shí)間:2023-04-04 10:58:47 來源:億速云 閱讀:114 作者:iii 欄目:開發(fā)技術(shù)

這篇文章主要介紹“在Spring環(huán)境中怎么正確關(guān)閉線程池”,在日常操作中,相信很多人在在Spring環(huán)境中怎么正確關(guān)閉線程池問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對(duì)大家解答”在Spring環(huán)境中怎么正確關(guān)閉線程池”的疑惑有所幫助!接下來,請(qǐng)跟著小編一起來學(xué)習(xí)吧!

線程池正確關(guān)閉的姿勢(shì)

在這一節(jié),先不討論應(yīng)用中線程池該如何優(yōu)雅關(guān)閉以達(dá)到優(yōu)雅停機(jī)的效果,只是簡單介紹一下線程池正確關(guān)閉的姿勢(shì)

為簡化討論的復(fù)雜性,本文的線程池均是指JDK中的java.util.concurrent.ThreadPoolExecutor

正確關(guān)閉線程池的關(guān)鍵是 shutdown + awaitTermination或者 shutdownNow + awaitTermination

一種可能的使用姿勢(shì)如下:

ExecutorService executorService = Executors.newFixedThreadPool(1);
executorService.execute(() -> {
    // do task
});

// 執(zhí)行shutdown,將會(huì)拒絕新任務(wù)提交到線程池;待執(zhí)行的任務(wù)不會(huì)取消,正在執(zhí)行的任務(wù)也不會(huì)取消,將會(huì)繼續(xù)執(zhí)行直到結(jié)束
executorService.shutdown();

// 執(zhí)行shutdownNow,將會(huì)拒絕新任務(wù)提交到線程池;取消待執(zhí)行的任務(wù),嘗試取消執(zhí)行中的任務(wù)
// executorService.shutdownNow();

// 超時(shí)等待線程池完畢
executorService.awaitTermination(3, TimeUnit.SECONDS);

一個(gè)任務(wù)會(huì)有如下幾個(gè)狀態(tài):

  • 未提交,此時(shí)可以將任務(wù)提交到線程池

  • 已提交未執(zhí)行,此時(shí)任務(wù)已在線程池的隊(duì)列中,等待著執(zhí)行

  • 執(zhí)行中,此時(shí)任務(wù)正在執(zhí)行

  • 執(zhí)行完畢

那么,執(zhí)行shutdown方法或shutdownNow方法之后,將會(huì)影響任務(wù)的狀態(tài)

shutdown

  • 拒絕新任務(wù)提交

  • 待執(zhí)行的任務(wù)不會(huì)取消

  • 正在執(zhí)行的任務(wù)也不會(huì)取消,將繼續(xù)執(zhí)行

shutdownNow

  • 拒絕新任務(wù)提交

  • 取消待執(zhí)行的任務(wù)

  • 嘗試取消執(zhí)行中的任務(wù)(僅僅是做嘗試,成功與否取決于是否響應(yīng)InterruptedException,以及對(duì)其做出的反應(yīng))

接下來看一下java doc對(duì)這兩個(gè)方法的描述:

shutdown: Initiates an orderly shutdown in which previously submitted tasks are executed, but no new tasks will be accepted. Invocation has no additional effect if already shut down.
This method does not wait for previously submitted tasks to complete execution. Use awaitTermination to do that.

shutdownNow: Attempts to stop all actively executing tasks, halts the processing of waiting tasks, and returns a list of the tasks that were awaiting execution.
This method does not wait for actively executing tasks to terminate. Use awaitTermination to do that.
There are no guarantees beyond best-effort attempts to stop processing actively executing tasks. For example, typical implementations will cancel via Thread.interrupt, so any task that fails to respond to interrupts may never terminate.

Java doc 提到,這兩個(gè)方法都不會(huì)等執(zhí)任務(wù)執(zhí)行完畢,如果需要等待,請(qǐng)使用awaitTermination。該方法帶有超時(shí)參數(shù):如果超時(shí)后任務(wù)仍然未執(zhí)行完畢,也不再等待。畢竟應(yīng)用總歸要停機(jī)重啟,而不可能無限等待下去,因此超時(shí)機(jī)制是提供給用戶的最后一道底線

綜上,shutdown(Now) + awaitTermination 確實(shí)是實(shí)現(xiàn)線程池優(yōu)雅關(guān)閉的關(guān)鍵

應(yīng)用中如何正確關(guān)閉線程池

這一節(jié)內(nèi)容其實(shí)才是本文要介紹的重心。上一小節(jié)內(nèi)容我們知道了如何優(yōu)雅關(guān)閉線程池,但那是一般意義上方法論指導(dǎo),如果將線程池運(yùn)用于我們的應(yīng)用中,譬如Spring Boot環(huán)境中,復(fù)雜度將會(huì)變得不一樣

本一節(jié),將會(huì)介紹線程池在Spring (Boot)環(huán)境中優(yōu)雅關(guān)閉遇到的一個(gè)問題跟挑戰(zhàn),以及解決方案

注:本節(jié)使用Spring Boot舉例,僅僅是因?yàn)樗膽?yīng)用面廣,受眾多,大家容易理解,并不代表只在該環(huán)境下才會(huì)出問題。在純Spring、甚至非Spring環(huán)境,都有可能出現(xiàn)問題

場景1

我們來假設(shè)一個(gè)場景,有了場景的鋪墊,對(duì)問題的理解會(huì)簡單一些

@Resource
private RedisTemplate<String, Integer> redisTemplate;

// 自定義線程池
public static ExecutorService executorService = Executors.newFixedThreadPool(1);

@GetMapping("/incr")
public void incr() {
    executorService.execute(() -> {
        // 依賴Redis進(jìn)行計(jì)數(shù)
        redisTemplate.opsForValue().increment("demo", 1L);
    });
}
  • 自定義線程池,用于異步任務(wù)的執(zhí)行。此處為演示方便使用Executors.newFixedThreadPool(1)生成了只有一個(gè)線程的線程池

  • 高并發(fā)請(qǐng)求/incr接口,每次請(qǐng)求該接口,都會(huì)往線程池中添加一個(gè)任務(wù),任務(wù)異步執(zhí)行的過程中依賴Redis

此時(shí),要求停機(jī)發(fā)布新版本,按照J(rèn)ava System#exit 無法退出程序的問題文章,我們知道了優(yōu)雅停機(jī)的一般步驟:

  • 切斷上游流量入口,確保不再有流量進(jìn)入到當(dāng)前節(jié)點(diǎn)

  • 向應(yīng)用發(fā)送kill 命令,在設(shè)定的時(shí)間內(nèi)待應(yīng)用正常關(guān)閉,若超時(shí)后應(yīng)用仍然存活,則使用kill -9命令強(qiáng)制關(guān)閉

  • 當(dāng)JVM接收到kill命令,會(huì)喚起應(yīng)用中所有的Shutdown Hooks,等待Shutdown Hooks執(zhí)行完畢便可以正常關(guān)機(jī);與此同時(shí),應(yīng)用會(huì)接著處理在途請(qǐng)求,以確保不會(huì)向客戶端拋出連接中斷異常,實(shí)現(xiàn)無感知發(fā)布

一切看起來很美好,然而&hellip;

當(dāng)JVM收到kill指令后,便會(huì)喚醒所有的Shutdown Hook,而其中有一個(gè)Shutdown Hook是Spring應(yīng)用在啟動(dòng)之初注冊(cè)的,它的作用是對(duì)Spring管理的Bean進(jìn)行回收,并銷毀IOC容器

那么問題就產(chǎn)生了:以我們的場景為例,線程池里的任務(wù)與Spring Shutdhwon Hook正在并發(fā)地執(zhí)行著,一旦任務(wù)執(zhí)行期依賴的資源先行被釋放,那任務(wù)執(zhí)行時(shí)必然會(huì)報(bào)錯(cuò)

在我們的場景中,就很有可能因?yàn)镽edis連接被回收,從而導(dǎo)致redisTemplate.opsForValue().increment("demo", 1L);拋出異常,執(zhí)行失敗

如圖示:

Jedis連接池先行被回收

在Spring環(huán)境中怎么正確關(guān)閉線程池

下一刻,線程池里的任務(wù)嘗試獲取Jedis連接,失敗并拋出異常

在Spring環(huán)境中怎么正確關(guān)閉線程池

在Spring環(huán)境中怎么正確關(guān)閉線程池

場景2

除了上述場景外,還有一個(gè)場景或許大家也經(jīng)常會(huì)碰到:本地啟動(dòng)一個(gè)定時(shí)任務(wù),按一定頻率將數(shù)據(jù)從DB加載到Cache中

例如:

ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);

scheduledExecutorService.scheduleWithFixedDelay(() -> {
    // load from db and put into cache
    // ...
    
}, 100, 100, TimeUnit.MILLISECONDS);
  • 每100ms向線程池里扔一個(gè)任務(wù)

  • 任務(wù)是:從DB中取出數(shù)據(jù),放入緩存(例如Local Cache,Redis)

在Spring Shutdown Hook執(zhí)行期間,新的任務(wù)仍然會(huì)產(chǎn)生,又或者舊的任務(wù)未執(zhí)行完畢,一旦嘗試獲取DB資源,就可能由于資源被回收而獲取失敗,拋出異常

此時(shí)的系統(tǒng)關(guān)閉已經(jīng)不優(yōu)雅&mdash;任務(wù)執(zhí)行有異常,這種異常可能對(duì)業(yè)務(wù)有損,我們應(yīng)盡量避免類似問題的產(chǎn)生,而不是抱著"算了吧,反正產(chǎn)生這個(gè)問題的概率很低",或者"算了吧,反正異常對(duì)我目前業(yè)務(wù)影響也不大"的態(tài)度,這是技術(shù)人的基本修養(yǎng),也是對(duì)自我提高的要求&mdash;目前業(yè)務(wù)影響不大,允許不優(yōu)先解決,但是期望掌握一種解決方案,將來有一天如果碰到了對(duì)業(yè)務(wù)損傷比較大的場景,可以很有底氣地說:我能行

解決方案

這個(gè)問題產(chǎn)生的根因,是Spring Shutdown Hook與線程池里的任務(wù)并發(fā)執(zhí)行,有可能使任務(wù)依賴的資源被提前回收導(dǎo)致的。那么一個(gè)很直白的思路即是:在切斷流量之后,能否讓線程池先關(guān)閉,再執(zhí)行Spring 的Shutdown Hook,避免依賴資源被提前回收?

順著這個(gè)思路,有三個(gè)問題需要解決:

  • 線程池如何關(guān)閉

  • 線程池如何感知Spring Shutdown Hook將要被執(zhí)行

  • 如何讓線程池先于Spring Shutdown Hook關(guān)閉

對(duì)于第一個(gè)問題,本文的上一個(gè)小節(jié)線程池正確關(guān)閉的姿勢(shì)已經(jīng)給出了解決方案:即shutdown(Now) + awaitTermination

對(duì)于第二個(gè)問題,Spring Shutdown Hook被觸發(fā)的時(shí)候,會(huì)主動(dòng)發(fā)出一些事件,我們只要監(jiān)聽這些的事件,就能夠做出相應(yīng)的反應(yīng)

對(duì)于第三個(gè)問題,我們只要在這些事件的監(jiān)聽器中先行將線程池關(guān)閉,再讓程序走接下來的關(guān)閉流程即可

二、三涉及到Spring 的Shutdown Hook 執(zhí)行過程,具體原理本篇按下不表,留待下一篇進(jìn)行分析

在Spring環(huán)境中怎么正確關(guān)閉線程池

從上圖中可以看出,只要在destroyBeans之前關(guān)閉線程池即可,因此,有兩種解決方案:

  • 監(jiān)聽Spring的ContextClosedEvent事件,在事件被觸發(fā)時(shí)關(guān)閉線程池

  • 實(shí)現(xiàn)Lifecycle接口,并在其stop方法中關(guān)閉線程池

此處以監(jiān)聽ContextClosedEvent為例:

@Component
public class ContextClosedHandler implements ApplicationListener<ContextClosedEvent> {

    @Override
    public void onApplicationEvent(ContextClosedEvent event) {
    	  // 獲取線程池
    	  // ...
    	  
    	  // 關(guān)閉線程池,并等待一段時(shí)間
        myExecutorService.shutdown();
        myExecutorService.awaitTermination(3, TimeUnit.SECONDS);
    }
}

此處大家或許能看出一些小問題:需要自行管理線程池。在Spring環(huán)境中,我們其實(shí)有更多的選擇:使用Spring提供的org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor,并將實(shí)例交給Spring管理

代碼如下:

// 將ThreadPoolTaskExecutor實(shí)例交給Spring管理
@Bean
public ThreadPoolTaskExecutor threadPoolTaskExecutor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setCorePoolSize(1);
    executor.setMaxPoolSize(1);
    
    // 告訴線程池,在銷毀之前執(zhí)行shutdown方法
    executor.setWaitForTasksToCompleteOnShutdown(true);
    // shutdown\shutdownNow 之后等待3秒
    executor.setAwaitTerminationSeconds(3);
    
    return executor;
}
@Component
public class ContextClosedHandler implements ApplicationListener<ContextClosedEvent> {
    // 直接注入
    @Resource
    private ThreadPoolTaskExecutor executor;

    @Override
    public void onApplicationEvent(ContextClosedEvent event) {
    		// 關(guān)閉線程池
        executor.destroy();
    }
}

注: ThreadPoolTaskExecutor的waitForTasksToCompleteOnShutdown + awaitTerminationSeconds等于ThreadPoolExecutor的shutdown + awaitTermination,且在定義線程池時(shí)就將優(yōu)雅關(guān)閉行為一同定義完畢,實(shí)現(xiàn)了高內(nèi)聚的目的

在Spring環(huán)境中怎么正確關(guān)閉線程池

在Spring環(huán)境中怎么正確關(guān)閉線程池

在Spring中使用ThreadPoolTaskExecutor,更便捷:

  • 不用再自行管理線程池,獲取的時(shí)候也很方便,直接注入即可

  • 在需要關(guān)閉的時(shí)候,直接調(diào)用destroy方法即可實(shí)現(xiàn)優(yōu)雅關(guān)閉

這樣,Spring就會(huì)等到線程池關(guān)閉(超時(shí))后,才會(huì)接著往下執(zhí)行Bean的銷毀、資源回收、應(yīng)用上下文關(guān)閉的邏輯,確保被依賴資源不會(huì)被提前回收掉

到此,關(guān)于“在Spring環(huán)境中怎么正確關(guān)閉線程池”的學(xué)習(xí)就結(jié)束了,希望能夠解決大家的疑惑。理論與實(shí)踐的搭配能更好的幫助大家學(xué)習(xí),快去試試吧!若想繼續(xù)學(xué)習(xí)更多相關(guān)知識(shí),請(qǐng)繼續(xù)關(guān)注億速云網(wǎng)站,小編會(huì)繼續(xù)努力為大家?guī)砀鄬?shí)用的文章!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請(qǐng)聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI