在Spring環(huán)境中正確關(guān)閉線程池的姿勢(shì)
前言
在Java System#exit 無(wú)法退出程序的問(wèn)題一文末尾提到優(yōu)雅停機(jī)的一種實(shí)現(xiàn)方案,要借助Shutdown Hook
進(jìn)行實(shí)現(xiàn),本文,將繼續(xù)探索優(yōu)雅停機(jī)中遇到的一些問(wèn)題:應(yīng)用中線程池的優(yōu)雅關(guān)閉
線程池正確關(guān)閉的姿勢(shì)
在這一節(jié),先不討論應(yīng)用中線程池該如何優(yōu)雅關(guān)閉以達(dá)到優(yōu)雅停機(jī)的效果,只是簡(jiǎn)單介紹一下線程池正確關(guān)閉的姿勢(shì)
為簡(jiǎn)化討論的復(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))
接下來(lái)看一下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ī)重啟,而不可能無(wú)限等待下去,因此超時(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è)問(wèn)題跟挑戰(zhàn),以及解決方案
注:本節(jié)使用Spring Boot舉例,僅僅是因?yàn)樗膽?yīng)用面廣,受眾多,大家容易理解,并不代表只在該環(huán)境下才會(huì)出問(wèn)題。在純Spring、甚至非Spring環(huán)境,都有可能出現(xiàn)問(wèn)題
場(chǎng)景1
我們來(lái)假設(shè)一個(gè)場(chǎng)景,有了場(chǎng)景的鋪墊,對(duì)問(wèn)題的理解會(huì)簡(jiǎn)單一些
@Resource private RedisTemplate<String, Integer> redisTemplate; // 自定義線程池 public static ExecutorService executorService = Executors.newFixedThreadPool(1); @GetMapping("/incr") public void incr() { executorService.execute(() -> { // 依賴(lài)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í)行的過(guò)程中依賴(lài)Redis
此時(shí),要求停機(jī)發(fā)布新版本,按照Java System#exit 無(wú)法退出程序的問(wèn)題文章,我們知道了優(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)無(wú)感知發(fā)布
一切看起來(lái)很美好,然而…
當(dāng)JVM收到kill指令后,便會(huì)喚醒所有的Shutdown Hook,而其中有一個(gè)Shutdown Hook是Spring應(yīng)用在啟動(dòng)之初注冊(cè)的,它的作用是對(duì)Spring管理的Bean進(jìn)行回收,并銷(xiāo)毀IOC容器
那么問(wèn)題就產(chǎn)生了:以我們的場(chǎng)景為例,線程池里的任務(wù)與Spring Shutdhwon Hook正在并發(fā)地執(zhí)行著,一旦任務(wù)執(zhí)行期依賴(lài)的資源先行被釋放,那任務(wù)執(zhí)行時(shí)必然會(huì)報(bào)錯(cuò)
在我們的場(chǎng)景中,就很有可能因?yàn)镽edis連接被回收,從而導(dǎo)致redisTemplate.opsForValue().increment("demo", 1L);
拋出異常,執(zhí)行失敗
如圖示:
Jedis連接池先行被回收
下一刻,線程池里的任務(wù)嘗試獲取Jedis連接,失敗并拋出異常
場(chǎng)景2
除了上述場(chǎng)景外,還有一個(gè)場(chǎng)景或許大家也經(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)雅—任務(wù)執(zhí)行有異常,這種異??赡軐?duì)業(yè)務(wù)有損,我們應(yīng)盡量避免類(lèi)似問(wèn)題的產(chǎn)生,而不是抱著"算了吧,反正產(chǎn)生這個(gè)問(wèn)題的概率很低",或者"算了吧,反正異常對(duì)我目前業(yè)務(wù)影響也不大"的態(tài)度,這是技術(shù)人的基本修養(yǎng),也是對(duì)自我提高的要求—目前業(yè)務(wù)影響不大,允許不優(yōu)先解決,但是期望掌握一種解決方案,將來(lái)有一天如果碰到了對(duì)業(yè)務(wù)損傷比較大的場(chǎng)景,可以很有底氣地說(shuō):我能行
解決方案
這個(gè)問(wèn)題產(chǎn)生的根因,是Spring Shutdown Hook與線程池里的任務(wù)并發(fā)執(zhí)行,有可能使任務(wù)依賴(lài)的資源被提前回收導(dǎo)致的。那么一個(gè)很直白的思路即是:在切斷流量之后,能否讓線程池先關(guān)閉,再執(zhí)行Spring 的Shutdown Hook,避免依賴(lài)資源被提前回收?
順著這個(gè)思路,有三個(gè)問(wèn)題需要解決:
- 線程池如何關(guān)閉
- 線程池如何感知Spring Shutdown Hook將要被執(zhí)行
- 如何讓線程池先于Spring Shutdown Hook關(guān)閉
對(duì)于第一個(gè)問(wèn)題,本文的上一個(gè)小節(jié)線程池正確關(guān)閉的姿勢(shì)已經(jīng)給出了解決方案:即shutdown(Now) + awaitTermination
對(duì)于第二個(gè)問(wèn)題,Spring Shutdown Hook被觸發(fā)的時(shí)候,會(huì)主動(dòng)發(fā)出一些事件,我們只要監(jiān)聽(tīng)這些的事件,就能夠做出相應(yīng)的反應(yīng)
對(duì)于第三個(gè)問(wèn)題,我們只要在這些事件的監(jiān)聽(tīng)器中先行將線程池關(guān)閉,再讓程序走接下來(lái)的關(guān)閉流程即可
二、三涉及到Spring 的Shutdown Hook 執(zhí)行過(guò)程,具體原理本篇按下不表,留待下一篇進(jìn)行分析
從上圖中可以看出,只要在destroyBeans
之前關(guān)閉線程池即可,因此,有兩種解決方案:
- 監(jiān)聽(tīng)Spring的ContextClosedEvent事件,在事件被觸發(fā)時(shí)關(guān)閉線程池
- 實(shí)現(xiàn)Lifecycle接口,并在其stop方法中關(guān)閉線程池
此處以監(jiān)聽(tīng)ContextClosedEvent
為例:
@Component public class ContextClosedHandler implements ApplicationListener<ContextClosedEvent> { @Override public void onApplicationEvent(ContextClosedEvent event) { // 獲取線程池 // ... // 關(guān)閉線程池,并等待一段時(shí)間 myExecutorService.shutdown(); myExecutorService.awaitTermination(3, TimeUnit.SECONDS); } }
此處大家或許能看出一些小問(wèn)題:需要自行管理線程池。在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); // 告訴線程池,在銷(xiāo)毀之前執(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中使用ThreadPoolTaskExecutor,更便捷:
- 不用再自行管理線程池,獲取的時(shí)候也很方便,直接注入即可
- 在需要關(guān)閉的時(shí)候,直接調(diào)用destroy方法即可實(shí)現(xiàn)優(yōu)雅關(guān)閉
這樣,Spring就會(huì)等到線程池關(guān)閉(超時(shí))后,才會(huì)接著往下執(zhí)行Bean的銷(xiāo)毀、資源回收、應(yīng)用上下文關(guān)閉的邏輯,確保被依賴(lài)資源不會(huì)被提前回收掉
總結(jié)
本篇以?xún)煞N實(shí)際場(chǎng)景為例,拋出了一個(gè)很切合實(shí)際項(xiàng)目的問(wèn)題:在Spring應(yīng)用中如何正確地關(guān)閉線程池。文中指出,如果非正常關(guān)閉將可能會(huì)產(chǎn)生異常的問(wèn)題,同時(shí)也分析了問(wèn)題產(chǎn)生的原因并給出了相應(yīng)的解決方案。下一篇,將會(huì)具體分析Spring Shutdown Hook執(zhí)行過(guò)程,與諸君共同探索其中的奧秘
思考
本文雖以"Spring環(huán)境中正確關(guān)閉線程池"為背景進(jìn)行討論,然而實(shí)際上思維還可以更發(fā)散一些,可以不限于Spring環(huán)境,也不限于"關(guān)閉線程池"這個(gè)行為。更一般化地,在一個(gè)應(yīng)用上下文環(huán)境中,許多的Bean有相互依賴(lài)的關(guān)系,這種依賴(lài)關(guān)系在應(yīng)用啟動(dòng)及應(yīng)用關(guān)閉之時(shí)需要格外地注意:在啟動(dòng)時(shí),被依賴(lài)的Bean需要先行構(gòu)造完畢;在關(guān)閉時(shí),被依賴(lài)的Bean需要靠后銷(xiāo)毀。依托這個(gè)思想,只要找到應(yīng)用上下文提供給我們的擴(kuò)展點(diǎn),就可以達(dá)到目的
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
Quartz實(shí)現(xiàn)JAVA定時(shí)任務(wù)的動(dòng)態(tài)配置的方法
這篇文章主要介紹了Quartz實(shí)現(xiàn)JAVA定時(shí)任務(wù)的動(dòng)態(tài)配置的方法,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-07-07SpringCloud Open feign 使用okhttp 優(yōu)化詳解
這篇文章主要介紹了SpringCloud Open feign 使用okhttp 優(yōu)化詳解,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2021-02-02Java?實(shí)現(xiàn)判定順序表中是否包含某個(gè)元素(思路詳解)
這篇文章主要介紹了Java?實(shí)現(xiàn)判定順序表中是否包含某個(gè)元素,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-06-06SpringBoot?實(shí)現(xiàn)CAS?Server統(tǒng)一登錄認(rèn)證的詳細(xì)步驟
??CAS(Central?Authentication?Service)中心授權(quán)服務(wù),是一個(gè)開(kāi)源項(xiàng)目,目的在于為Web應(yīng)用系統(tǒng)提供一種可靠的單點(diǎn)登錄,這篇文章主要介紹了SpringBoot?實(shí)現(xiàn)CAS?Server統(tǒng)一登錄認(rèn)證,需要的朋友可以參考下2024-02-02SpringMVC攔截器實(shí)現(xiàn)登錄認(rèn)證
這篇文章主要介紹了SpringMVC攔截器實(shí)現(xiàn)登錄認(rèn)證的相關(guān)資料,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-11-11Java 1.8使用數(shù)組實(shí)現(xiàn)循環(huán)隊(duì)列
這篇文章主要為大家詳細(xì)介紹了Java 1.8使用數(shù)組實(shí)現(xiàn)循環(huán)隊(duì)列,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-10-10java定義二維數(shù)組的幾種寫(xiě)法(小結(jié))
下面小編就為大家?guī)?lái)一篇java定義二維數(shù)組的幾種寫(xiě)法(小結(jié))。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2016-10-10springboot+gradle 構(gòu)建多模塊項(xiàng)目的步驟
這篇文章主要介紹了springboot+gradle 構(gòu)建多模塊項(xiàng)目的步驟,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-05-05SpringBoot集成JPA持久層框架,簡(jiǎn)化數(shù)據(jù)庫(kù)操作
JPA(Java Persistence API)意即Java持久化API,是Sun官方在JDK5.0后提出的Java持久化規(guī)范。主要是為了簡(jiǎn)化持久層開(kāi)發(fā)以及整合ORM技術(shù),結(jié)束Hibernate、TopLink、JDO等ORM框架各自為營(yíng)的局面。JPA是在吸收現(xiàn)有ORM框架的基礎(chǔ)上發(fā)展而來(lái),易于使用,伸縮性強(qiáng)。2021-06-06