前言
上篇文章潮太,我們討論了在Spring環(huán)境中正確關(guān)閉線程池的姿勢(shì)忠藤,拋出了問(wèn)題并給出了解決方案。本篇办铡,將接著討論解決方案背后的原理:Spring Shutdown Hook工作機(jī)制
源碼解析
源碼基于Spring Boot 2.1.0.RELEASE
注冊(cè)Spring Shutdown Hook的時(shí)機(jī)
首先要找到入口在哪双抽,即Spring Shutdown Hook是在哪注冊(cè)的百框,很容易猜想,應(yīng)該是在應(yīng)用啟動(dòng)過(guò)程中注冊(cè)的荠诬,找到如下源碼位置:org.springframework.boot.SpringApplication#refreshContext
(Spring Boot)
Spring Boot 在啟動(dòng)過(guò)程中琅翻,刷新Context之后,如果registerShutdownHook
開(kāi)啟[默認(rèn)為true]柑贞,則會(huì)注冊(cè)一個(gè)Shutdown Hook
org.springframework.context.support.AbstractApplicationContext#registerShutdownHook (spring-context) 如下:
這里有一點(diǎn)需要注意的是:提供Spring Shutdown Hook能力的是spring-context
方椎,即spring framework
本身的能力,但是將shutdown hook注冊(cè)進(jìn)JVM shutdown hook的行為钧嘶,卻是Spring Boot
提供的棠众。也就是說(shuō),如果在純Spring環(huán)境下有决,需要自己手動(dòng)調(diào)用AbstractApplicationContext#registerShutdownHook
注冊(cè)shutdown hook來(lái)支持Spring的優(yōu)雅關(guān)閉
畫(huà)外音:哪有什么歲月靜好闸拿,只不過(guò)有人(Spring Boot) 替你負(fù)重前行
Spring Shutdown Hook的邏輯
接下來(lái)看看Spring Shutdown Hook的具體實(shí)現(xiàn)邏輯,在org.springframework.context.support.AbstractApplicationContext#doClose
protected void doClose() {
if (this.active.get() && this.closed.compareAndSet(false, true)) {
// ...(省略)
// 發(fā)布Spring 應(yīng)用上下文的關(guān)閉事件书幕,讓監(jiān)聽(tīng)器們有機(jī)會(huì)在應(yīng)用關(guān)閉之前做出一些響應(yīng)
publishEvent(new ContextClosedEvent(this));
// 執(zhí)行l(wèi)ifecycleProcessor的關(guān)閉方法新荤,讓Lifecycle們有機(jī)會(huì)在應(yīng)用關(guān)閉之前做出一些響應(yīng)
this.lifecycleProcessor.onClose();
// 銷毀IOC容器里所有單例Bean
destroyBeans();
// 關(guān)閉BeanFactory
closeBeanFactory();
// 勾子函數(shù),讓子類實(shí)現(xiàn)后做各自的資源清理台汇,比如ServletWebServerApplicationContext會(huì)實(shí)現(xiàn)該勾子函數(shù)關(guān)閉內(nèi)嵌的WebServer(Tomcat)
onClose();
this.active.set(false);
}
}
Spring Shutdown Hook 一共做了5件事:
- 發(fā)布Spring應(yīng)用上下文的關(guān)閉事件苛骨,讓監(jiān)聽(tīng)器們有機(jī)會(huì)在應(yīng)用關(guān)閉之前做出一些響應(yīng)
- 執(zhí)行l(wèi)ifecycleProcessor的關(guān)閉方法,讓Lifecycle們有機(jī)會(huì)在應(yīng)用關(guān)閉之前做出一些響應(yīng)
- 銷毀IOC容器里所有單例Bean
- 關(guān)閉BeanFactory
- 執(zhí)行勾子函數(shù)苟呐,子類實(shí)現(xiàn)后做各自的資源清理痒芝,比如ServletWebServerApplicationContext會(huì)實(shí)現(xiàn)該勾子函數(shù)關(guān)閉內(nèi)嵌的WebServer(Tomcat)
不得不贊稱,站在上層的角度去理解牵素,該段邏輯非常清晰严衬,這樣的代碼鮮明地為我們展示了編碼原則:一個(gè)方法內(nèi)部,代碼盡量保持在同一抽象層次
其中第1笆呆、第2件事请琳,正是我們?cè)?a href="http://www.reibang.com/p/9be91ae48157" target="_blank">Spring環(huán)境中正確關(guān)閉線程池的姿勢(shì)利用到的解決方案:即在第3件事情開(kāi)始前粱挡,通過(guò)某些機(jī)制通知應(yīng)用程序?qū)κ录龀鲰憫?yīng)
第1件事與第2件事看起來(lái)很像,都是讓?xiě)?yīng)用關(guān)閉之前做出一些響應(yīng)单起,但是有使用場(chǎng)景的區(qū)別:
- ContextClosedEvent是應(yīng)用級(jí)別的事件抱怔,因此對(duì)之做出的響應(yīng)更適用于全局性的行為
- Lifecycle一般是Bean級(jí)別的通知劣坊,因此對(duì)之做出的響應(yīng)更適用于單個(gè)Bean的行為
接下來(lái)看第3件事:org.springframework.context.support.AbstractApplicationContext#destroyBeans
這是個(gè)模板方法嘀倒,默認(rèn)情況下會(huì)銷毀IOC容器里的單例Bean,子類可以覆蓋它并添加一些額外的行為局冰,但是迄今為止测蘑,也沒(méi)有子類覆蓋該方法
org.springframework.beans.factory.support.DefaultListableBeanFactory#destroySingletons 方法如下:
destroySingletons
是個(gè)重載方法,核心邏輯在父類DefaultSingletonBeanRegistry中康二,調(diào)用完父類方法后就清理一下本類涉及的的一些本地緩存數(shù)據(jù)碳胳。我們接著看父類方法的邏輯:
// org.springframework.beans.factory.support.DefaultSingletonBeanRegistry#destroySingletons
public void destroySingletons() {
// ...(省略)
String[] disposableBeanNames;
// disposableBeans 是個(gè)Map,Key為bean name沫勿,value為disposable bean實(shí)例
// 即 <beanName, disposableInstance>
// private final Map<String, Object> disposableBeans = new LinkedHashMap<>();
synchronized (this.disposableBeans) {
disposableBeanNames = StringUtils.toStringArray(this.disposableBeans.keySet());
}
// 依次銷毀 disposableInstances
for (int i = disposableBeanNames.length - 1; i >= 0; i--) {
destroySingleton(disposableBeanNames[i]);
}
// 清除本類使用到的一些本地緩存
this.containedBeanMap.clear();
this.dependentBeanMap.clear();
this.dependenciesForBeanMap.clear();
// 清除單例緩存
clearSingletonCache();
}
/**
* Clear all cached singleton instances in this registry.
* @since 4.3.15
*/
protected void clearSingletonCache() {
synchronized (this.singletonObjects) {
this.singletonObjects.clear();
this.singletonFactories.clear();
this.earlySingletonObjects.clear();
this.registeredSingletons.clear();
this.singletonsCurrentlyInDestruction = false;
}
}
這個(gè)段方法的邏輯也很簡(jiǎn)單:
- 拿到所有的disposable beans(即實(shí)現(xiàn)了DisposableBean接口的bean)挨约,依次執(zhí)行destroySingleton方法,進(jìn)行資源回收
- 清除本類使用到的一些本地緩存
- 清除單例緩存
2产雹、3清除緩存的動(dòng)作很簡(jiǎn)單诫惭,就是調(diào)用Map#clear\Set#clear
方法,將集合清空
這里有兩個(gè)緩存Map需要注意:dependentBeanMap
與dependenciesForBeanMap
蔓挖,它們的定義如下:
/** Map between dependent bean names: bean name to Set of dependent bean names. */
private final Map<String, Set<String>> dependentBeanMap = new ConcurrentHashMap<>(64);
/** Map between depending bean names: bean name to Set of bean names for the bean's dependencies. */
private final Map<String, Set<String>> dependenciesForBeanMap = new ConcurrentHashMap<>(64);
- dependentBeanMap: Bean名稱和所有依賴于Bean的名稱的映射關(guān)系夕土,即:誰(shuí)依賴我
- dependenciesForBeanMap: Bean名稱和Bean所依賴的所有名稱的映射關(guān)系,即:我依賴誰(shuí)
命名上很像瘟判,不好理解怨绣。我舉個(gè)例子幫助理解:假設(shè)A依賴B(即A->B),A依賴C(即A->C)拷获,那么篮撑,
- dependentBeanMap: <B, [A]>與<C, [A]>
- dependenciesForBeanMap: <A, [B,C]>
此處請(qǐng)先將兩個(gè)Map映射關(guān)系記住,至于具體作用會(huì)在下文解釋
還有一個(gè)緩存Map: containedBeanMap匆瓜,定義如下:
/** Map between containing bean names: bean name to Set of bean names that the bean contains. */
private final Map<String, Set<String>> containedBeanMap = new ConcurrentHashMap<>(16);
- containedBeanMap: Bean名稱和Bean所包含的所有Bean的名稱的映射關(guān)系赢笨,即:我包含誰(shuí)
這種"我包含誰(shuí)"的關(guān)系在主流的Annotation-Base的場(chǎng)景下已經(jīng)比較少出現(xiàn)了,要構(gòu)造這種映射關(guān)系陕壹,需要是XML-Base质欲,
假設(shè)Foo包含Bar,需要通過(guò)如下Spring的配置文件進(jìn)行配置糠馆,才會(huì)將這種"包含"關(guān)系放入containedBeanMap
public class Foo {
private Bar bar;
public Foo(Bar bar) {
this.bar = bar;
}
}
public class Bar {
}
// Spring 配置文件
<bean id="foo" class="com.example.demo.Foo">
<constructor-arg>
<bean class="com.example.demo.Bar"/>
</constructor-arg>
</bean>
從另一個(gè)角度看嘶伟,這也是一種依賴關(guān)系:Foo依賴Bar。由于使用該場(chǎng)景的人越來(lái)越少又碌,因此簡(jiǎn)單了解一下containedBeanMap的含義即可
接下來(lái)看org.springframework.beans.factory.support.DefaultListableBeanFactory#destroySingleton方法九昧,銷毀單個(gè)bean绊袋,注意跟上文提到的方法的區(qū)別,上文是destroySingletons
同樣的铸鹰,destroySingleton也是個(gè)重載方法癌别,核心邏輯也是在父類DefaultSingletonBeanRegistry中,接著看:org.springframework.beans.factory.support.DefaultSingletonBeanRegistry#destroySingleton
接下來(lái)看org.springframework.beans.factory.support.DefaultSingletonBeanRegistry#destroyBean方法:
// org.springframework.beans.factory.support.DefaultSingletonBeanRegistry#destroyBean
protected void destroyBean(String beanName, @Nullable DisposableBean bean) {
// 1. 首先回收所有依賴"我"的beans
Set<String> dependencies;
synchronized (this.dependentBeanMap) {
// Within full synchronization in order to guarantee a disconnected Set
dependencies = this.dependentBeanMap.remove(beanName);
}
for (String dependentBeanName : dependencies) {
// 遞歸調(diào)用DefaultSingletonBeanRegistry#destroySingleton
destroySingleton(dependentBeanName);
}
// 2. 執(zhí)行DisposableBean的destroy方法蹋笼,進(jìn)行資源的回收
bean.destroy();
// 3. 回收"我"包含的所有beans
Set<String> containedBeans;
synchronized (this.containedBeanMap) {
// Within full synchronization in order to guarantee a disconnected Set
containedBeans = this.containedBeanMap.remove(beanName);
}
if (containedBeans != null) {
for (String containedBeanName : containedBeans) {
destroySingleton(containedBeanName);
}
}
// 4. 解除"我"對(duì)其他Bean的依賴關(guān)系(dependentBeanMap)
synchronized (this.dependentBeanMap) {
for (Iterator<Map.Entry<String, Set<String>>> it = this.dependentBeanMap.entrySet().iterator(); it.hasNext();) {
Map.Entry<String, Set<String>> entry = it.next();
Set<String> dependenciesToClean = entry.getValue();
dependenciesToClean.remove(beanName);
if (dependenciesToClean.isEmpty()) {
it.remove();
}
}
}
// 5. 解除"我"對(duì)其他Bean的依賴關(guān)系(dependenciesForBeanMap)
this.dependenciesForBeanMap.remove(beanName);
}
這個(gè)方法一共做了5件事:
- 首先回收所有依賴"我"的beans
- 執(zhí)行DisposableBean的destroy方法展姐,進(jìn)行資源的回收
- 回收"我"包含的所有beans(containedBeanMap)
- 解除"我"對(duì)其他Bean的依賴關(guān)系(dependentBeanMap)
- 解除"我"對(duì)其他Bean的依賴關(guān)系(dependenciesForBeanMap)
為了便于理解,我舉個(gè)例子來(lái)分析這整個(gè)過(guò)程:
- 假設(shè)A\B\C三個(gè)Bean剖毯,A\C都是普通的被Spring管理的Bean圾笨,B實(shí)現(xiàn)了
DisposableBean
接口,同樣被Spring管理 - A依賴B逊谋,B依賴C
上圖顯示初始狀態(tài)下的依賴關(guān)系擂达,以及三個(gè)Map各自的數(shù)據(jù)
此時(shí),要銷毀Bean B
-
首先回收所有依賴"我"的beans胶滋。通過(guò)dependentBeanMap找到"誰(shuí)依賴我"板鬓,遞歸執(zhí)行
destroySingleton
將依賴我的對(duì)象先回收掉,由圖可知A依賴了B究恤,因此先回收A俭令。該步驟執(zhí)行完之后,狀態(tài)如下示:image-20200723133055164 執(zhí)行DisposableBean的destroy方法丁溅,進(jìn)行資源的回收唤蔗。此處,要執(zhí)行B的destroy方法窟赏,完成資源的回收妓柜。一旦該方法執(zhí)行完畢,說(shuō)明B就已經(jīng)完成其使命涯穷,可以被回收掉
回收"我"包含的所有beans(containedBeanMap)棍掐。 由于此處不構(gòu)造containedBeanMap,為空拷况,此步驟跳過(guò)
解除"我"對(duì)其他Bean的依賴關(guān)系(dependentBeanMap)作煌。B被銷毀之后,已經(jīng)是一個(gè)"無(wú)用"的Bean赚瘦,但是它本身可能還引用著其它的Bean粟誓,這種引用關(guān)系仍然被保存在dependentBeanMap里,因此需要把這種引用關(guān)系斷掉起意,來(lái)保證邏輯語(yǔ)義的正確
解除"我"對(duì)其他Bean的依賴關(guān)系(dependenciesForBeanMap)鹰服。同第4步,引用關(guān)系仍然可能被保存在dependenciesForBeanMap里,因此需要把這種引用關(guān)系斷掉悲酷,來(lái)保證邏輯語(yǔ)義的正確
第4套菜、第5件事是從不同的Map中斷掉這種引用關(guān)系,因此本質(zhì)上是同一回事设易。經(jīng)過(guò)4逗柴、5之后,如圖示:
注意:本文中提及的解除引用關(guān)系是指在Map上把依賴關(guān)系給刪除顿肺,而不是真正把對(duì)象間的引用給解除戏溺;
同理,銷毀(回收)Bean同樣指的是執(zhí)行destroy方法進(jìn)行了資源的回收挟冠,并不是真的把Bean給銷毀于购、回收
至此,Spring Shutdown Hook整個(gè)執(zhí)行過(guò)程我們已經(jīng)分析完畢知染,為了更好理解,接下來(lái)會(huì)用上篇文章的案例來(lái)分析Spring Shutdown Hook的執(zhí)行過(guò)程
案例解析
為了閱讀的連續(xù)性斑胜,此處再把案例闡述一遍
@Resource
private RedisTemplate<String, Integer> redisTemplate;
// org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor
@Resource
private ThreadPoolTaskExecutor executor;
@GetMapping("/incr")
public void incr() {
executor.execute(() -> {
// 依賴Redis進(jìn)行計(jì)數(shù)
redisTemplate.opsForValue().increment("demo", 1L);
});
}
- 使用Spring的ThreadPoolTaskExecutor控淡,用于異步任務(wù)的執(zhí)行
- 高并發(fā)請(qǐng)求/incr接口,每次請(qǐng)求該接口止潘,都會(huì)往線程池中添加一個(gè)任務(wù)掺炭,任務(wù)異步執(zhí)行的過(guò)程中依賴Redis
此時(shí),上游流量被切斷且應(yīng)用程序收到停機(jī)請(qǐng)求凭戴,在應(yīng)用啟動(dòng)之初注冊(cè)的Spring Shutdown Hook被激活
- 我們此處并不自定義上篇文章中提到的ContextClosedEvent涧狮,也不實(shí)現(xiàn)Lifecycle接口,因此
發(fā)布Spring應(yīng)用上下文的關(guān)閉事件
么夫、執(zhí)行l(wèi)ifecycleProcessor的關(guān)閉方法
這兩個(gè)過(guò)程略過(guò)(如果有疑問(wèn):這樣做者冤,上篇文章提到的問(wèn)題不就出現(xiàn)了么?不就不能實(shí)現(xiàn)優(yōu)雅關(guān)閉線程池了档痪?別急涉枫,下面會(huì)有答案) - 接著會(huì)銷毀所有實(shí)現(xiàn)了DisposableBean的Bean,很巧的是腐螟,ThreadPoolTaskExecutor與JedisConnectionFactory都實(shí)現(xiàn)了該接口愿汰,因此,依賴關(guān)系如圖所示:
org.springframework.data.redis.connection.jedis.JedisConnectionFactory#destroy
org.springframework.scheduling.concurrent.ExecutorConfigurationSupport#destroy
按照我們上文的分析乐纸,ThreadPoolTaskExecutor衬廷、JedisConnectionFactory的destroy方法都會(huì)被執(zhí)行:
ThreadPoolTaskExecutor#destroy: 執(zhí)行線程池的優(yōu)雅關(guān)閉
JedisConnectionFactory#destroy: 關(guān)閉Jedis連接池,回收J(rèn)edis連接
那ThreadPoolTaskExecutor與JedisConnectionFactory執(zhí)行destroySingleton
方法的先后不同汽绢,會(huì)導(dǎo)致結(jié)果的不同嗎吗跋?
- 假設(shè)ThreadPoolTaskExecutor先執(zhí)行。此時(shí)XXXController會(huì)先被destroy庶喜,然后執(zhí)行ThreadPoolTaskExecutor#destroy小腊,由于支持優(yōu)雅關(guān)閉救鲤,任務(wù)理論上已經(jīng)執(zhí)行完畢,不再需要使用到RedisTemplate秩冈,因此這種情況OK
- 假設(shè)JedisConnectionFactory行先執(zhí)行本缠。此時(shí)RedisTemplate會(huì)先要求被destroy,進(jìn)而引發(fā)XXXController與ThreadPoolTaskExecutor先行被destroy入问。此時(shí)就進(jìn)入了第一種情況丹锹,因此這種情況也是OK的
可以發(fā)現(xiàn),無(wú)論ThreadPoolTaskExecutor芬失、JedisConnectionFactory誰(shuí)先執(zhí)行destroySingleton
楣黍,結(jié)果都是一樣的,都能使得線程池被優(yōu)雅關(guān)閉棱烂,根本原因就是Spring會(huì)找到引用鏈中的頭節(jié)點(diǎn)先行銷毀租漂,然后依著引用鏈依次銷毀Bean,使得最底層被依賴的對(duì)象最晚被銷毀
那么為什么上篇文章還會(huì)出現(xiàn)Spring環(huán)境下線程池未優(yōu)雅關(guān)閉的問(wèn)題颊糜?
那是因?yàn)榱ㄖ危芏啻a會(huì)直接使用自定義的JDK線程池,未被Spring管理衬鱼,也沒(méi)有找到合適的地方執(zhí)行shutdown(Now)
+ awaitTermination
业筏。Spring Shutdown Hook執(zhí)行的時(shí)候,只能找到它管理的Bean進(jìn)行銷毀鸟赫,而我們使用的自定義的JDK線程池既不被Spring管理蒜胖,也沒(méi)有實(shí)現(xiàn)DisposableBean,Spring必然"看不見(jiàn)"該線程池的存在抛蚤,直接就把JedisConnectionFactory給回收了台谢,導(dǎo)致線程池里的任務(wù)獲取連接失敗
所以你瞧,使用ThreadPoolTaskExecutor還有這種福利霉颠,真是個(gè)意外的驚喜对碌,建議大家在Spring環(huán)境中都使用它代替直接使用JDK線程池類。 當(dāng)然蒿偎,如果有定制線程池的需要朽们,也可以自定義線程池類,然后再實(shí)現(xiàn)DisposableBean接口同時(shí)把相應(yīng)的destroy方法實(shí)現(xiàn)诉位,同時(shí)將實(shí)例交給Spring管理骑脱,效果也是等價(jià)的
那些非DisposableBean beans是如何銷毀的?
需要實(shí)現(xiàn)資源回收的Bean苍糠,需要關(guān)注Bean銷毀事件的Bean才需要實(shí)現(xiàn)DisposableBean接口叁丧。我們一般開(kāi)發(fā)過(guò)程中使用到的無(wú)狀態(tài)的Controller、Service,是不需要實(shí)現(xiàn)DisposableBean接口的--->我們何時(shí)關(guān)心過(guò)它們的銷毀呢拥娄?所以蚊锹,我們不關(guān)心,Spring當(dāng)然也不關(guān)心稚瘾,Spring Shutdown Hook 的第3件事"銷毀IOC容器里所有單例Bean"牡昆,只是執(zhí)行DisposableBean的destroy方法完成資源回收工作,以及清空各種依賴關(guān)系的Map和Singleon Cache摊欠,但對(duì)象本身并沒(méi)有真實(shí)被銷毀丢烘。因此對(duì)于非DisposableBean beans,在接下來(lái)應(yīng)用關(guān)閉之后就自動(dòng)死亡
總結(jié)
本篇文章主要分析了Spring Shutdown Hook的執(zhí)行流程些椒,從源碼層面可以看出作者的代碼功力非常強(qiáng)播瞳,考慮到了多種擴(kuò)展角度(擴(kuò)展點(diǎn)機(jī)制、模板方法免糕、勾子方法)赢乓,代碼從布局上也非常清晰,同一抽象語(yǔ)義的代碼在同一個(gè)方法里说墨,易于理解跟閱讀骏全,這是非常值得我們學(xué)習(xí)的地方(敲重點(diǎn):能從源碼中學(xué)到什么?)尼斧。其次從功能層面可以看到,做為一個(gè)成熟的框架陈醒,Spring考慮的非常全面:哪些Bean需要先銷毀哪些Bean需要后銷毀令漂,哪些Bean需要執(zhí)行資源回收方法身弊,哪些Bean不需要執(zhí)行資源回收方法都是有考量的,資源回收之后還清理各種本地緩存和映射關(guān)系烛恤,確保程序邏輯語(yǔ)義的正確。正是Spring考慮的多余耽,所以我們才可以心安理得考慮的少:哪有什么歲月靜好缚柏,只不過(guò)有人替你負(fù)重前行