在 JAVA 的世界里遨游,如果能擁有一雙善于發(fā)現(xiàn)的眼睛,有很多東西留心去看,外加耐心助力,仔細(xì)去品,往往會(huì)品出不一樣的味道。
通過本次分享,能讓你輕松 get 如下幾點(diǎn),絕對收獲滿滿。
a)如何讓 Java 程序?qū)崿F(xiàn)優(yōu)雅停服?有思想才是硬道理!
b)addShutdownHook 的使用場景?會(huì)用才是王道!
c)addShutdownHook 鉤子函數(shù)到底是個(gè)啥?刨根問底!
1. 如何讓 Java 程序?qū)崿F(xiàn)優(yōu)雅停服?
無論是自研基礎(chǔ)服務(wù)框架,還是分析開源項(xiàng)目源碼,細(xì)心的 Java 開發(fā)同學(xué),都會(huì)發(fā)現(xiàn) Runtime.getRuntime().addShutdownHook 這么一句代碼的身影,這句到底是干什么用的?
接下來就一起細(xì)品,看看它香不香?
阿里開源的數(shù)據(jù)同步神器 Canal 啟動(dòng)時(shí)的部分源碼:

Apache 麾下的用于海量日志收集的 Flume 啟動(dòng)時(shí)的部分源碼:

仰望了一下開源的項(xiàng)目,不妨從中提煉一下共性(同樣的代碼遇到多次,勢必會(huì)品出味道),寫段代碼跑跑看(站在 flume 源碼的肩膀上,起飛)。
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* 體驗(yàn) Java 優(yōu)雅停服
*
* @author 一猿小講
*/
public class Application {
/**
* 監(jiān)控服務(wù)
*/
private ScheduledThreadPoolExecutor monitorService;
public Application() {
monitorService = new ScheduledThreadPoolExecutor(1);
}
/**
* 啟動(dòng)監(jiān)控服務(wù),監(jiān)控一下內(nèi)存信息
*/
public void start() {
System.out.println(String.format("啟動(dòng)監(jiān)控服務(wù) %s", Thread.currentThread().getId()));
monitorService.scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
System.out.println(String.format("最大內(nèi)存: %dm 已分配內(nèi)存: %dm 已分配內(nèi)存中的剩余空間: %dm 最大可用內(nèi)存: %dm",
Runtime.getRuntime().maxMemory() / 1024 / 1024,
Runtime.getRuntime().totalMemory() / 1024 / 1024,
Runtime.getRuntime().freeMemory() / 1024 / 1024,
(Runtime.getRuntime().maxMemory() - Runtime.getRuntime().totalMemory() +
Runtime.getRuntime().freeMemory()) / 1024 / 1024));
}
}, 2, 2, TimeUnit.SECONDS);
}
/**
* 釋放資源(代碼來源于 flume 源碼)
* 主要用于關(guān)閉線程池(看不懂的同學(xué)莫糾結(jié),當(dāng)做黑盒去對待)
*/
public void stop() {
System.out.println(String.format("開始關(guān)閉線程池 %s", Thread.currentThread().getId()));
if (monitorService != null) {
monitorService.shutdown();
try {
monitorService.awaitTermination(10, TimeUnit.SECONDS);
} catch (InterruptedException e) {
System.err.println("Interrupted while waiting for monitor service to stop");
}
if (!monitorService.isTerminated()) {
monitorService.shutdownNow();
try {
while (!monitorService.isTerminated()) {
monitorService.awaitTermination(10, TimeUnit.SECONDS);
}
} catch (InterruptedException e) {
System.err.println("Interrupted while waiting for monitor service to stop");
}
}
}
System.out.println(String.format("線程池關(guān)閉完成 %s", Thread.currentThread().getId()));
}
/**
* 應(yīng)用入口
*/
public static void main(String[] args) {
Application application = new Application();
// 啟動(dòng)服務(wù)(每隔一段時(shí)間監(jiān)控輸出一下內(nèi)存信息)
application.start();
// 添加鉤子,實(shí)現(xiàn)優(yōu)雅停服(主要驗(yàn)證鉤子的作用)
final Application appReference = application;
Runtime.getRuntime().addShutdownHook(new Thread("shutdown-hook") {
@Override
public void run() {
System.out.println("接收到退出的訊號(hào),開始打掃戰(zhàn)場,釋放資源,完成優(yōu)雅停服");
appReference.stop();
}
});
System.out.println("服務(wù)啟動(dòng)完成");
}
}
經(jīng)常讀文的我很清楚,耐心讀文章中源碼的同學(xué)應(yīng)該很少,所以我還是用圖給你簡單捋一捋。

標(biāo)注1:start 方法利用線程池啟動(dòng)一個(gè)線程去定時(shí)監(jiān)控內(nèi)存信息;
標(biāo)注2:stop 方法用于在退出程序之前,進(jìn)行關(guān)閉線程池進(jìn)而釋放資源。
程序跑起來,效果如下。

當(dāng)進(jìn)行 kill 操作時(shí),程序確實(shí)進(jìn)行了資源釋放,效果確實(shí)很優(yōu)雅。

一切看似那么自然,一切又是那么完美,這是真的嗎?殺進(jìn)程時(shí)候如果用 kill -9,這種情況下會(huì)發(fā)生什么現(xiàn)象呢?

嗚呼!結(jié)果不會(huì)騙人的,當(dāng)用 kill -9 的時(shí)候,就顯得很粗暴了,壓根不管什么資源釋放,不管三七二十一,就是終止程序。
估計(jì)很多同學(xué),都擅長用 kill -9 進(jìn)行殺進(jìn)程,為了線上的應(yīng)用安全,還是用 kill -15 命令殺進(jìn)程吧,這樣會(huì)給應(yīng)用留點(diǎn)時(shí)間去打掃一下戰(zhàn)場,釋放一下資源。
好了,通過仔細(xì)品味,借助 JDK 自帶的 addShutdownHook 來助力應(yīng)用,確實(shí)能讓線上服務(wù)跑起來很優(yōu)雅。
有思想才是硬道理!
2. addShutdownHook 的使用場景?
通過代碼試驗(yàn),能夠感知 addShutdownHook(new Thread(){}) 是 JVM 銷毀前要執(zhí)行的一個(gè)線程,那么只要是涉及到資源回收的場景,應(yīng)該都可以滿足,下面簡單列舉幾個(gè)。
a)數(shù)據(jù)同步神器 Canal 借助它,來進(jìn)行關(guān)閉 socket 鏈接、釋放 canal 的工作節(jié)點(diǎn)、清理緩存信息等;
b)海量日志收集 Flume 借助它,來實(shí)現(xiàn)線程池資源關(guān)閉、工作線程停止等;
c)在應(yīng)用正常退出時(shí),執(zhí)行特定的業(yè)務(wù)邏輯、關(guān)閉資源等操作。
d)在 OOM 宕機(jī)、 CTRL+C、或執(zhí)行 kill pid,導(dǎo)致 JVM 非正常退出時(shí),加入必要的挽救措施成為可能。
其實(shí),在 Java 的世界里遨游,只有想不到的,沒有做不到的!
3. addShutdownHook 鉤子函數(shù)是個(gè)啥?
刨根還要問到底!

Hook 翻譯過來是「鉤子」的意思,那顧名思義就是用來掛東西的。

如圖所示,在現(xiàn)實(shí)生活中,要制作臘肉,首先用鉤子把肉勾住,然后掛在竹竿上,這應(yīng)該是鉤子的作用。
生活如此,一切設(shè)計(jì)理念都源于生活,在 Java 的世界里,亦是如此。

如上圖 Runtime 的源碼所示,遵循 Java 的核心思想「一切皆是對象」,那么可以把 addShutdownHook 方法可以視作掛鉤子,其實(shí)稱之為鉤子函數(shù)會(huì)好一些,而現(xiàn)實(shí)生活中的肉就可以抽象為釋放資源的線程。
只要有這個(gè)鉤子函數(shù),對外就提供了擴(kuò)展能力,研發(fā)人員就可以往鉤子上掛各種自定義的場景實(shí)現(xiàn),這種設(shè)計(jì)你細(xì)品那絕對是香!這也就是 Canal、Flume、Tomcat 等不同應(yīng)用,在優(yōu)雅停服時(shí)有著不同的實(shí)現(xiàn)的原因吧。
大白話,鉤子函數(shù)有了,想掛什么東西,根據(jù)心情自己定就好了。
再深入去刨會(huì)發(fā)現(xiàn),由于底層數(shù)據(jù)結(jié)構(gòu)采用 Map 來進(jìn)行存儲(chǔ),那么就支持研發(fā)人員掛多個(gè) shutdownHook 的實(shí)現(xiàn),又帶來了無限的可能性(又帶來了無限的「刺激」,自己好好去體會(huì))。

好了,避免頭大,就刨到這兒吧,感興趣的可自行順著思路繼續(xù)刨下去。
4. 寄語,寫在最后
作為研發(fā)人員:要擁有一雙善于發(fā)現(xiàn)的眼睛,要善于發(fā)現(xiàn)代碼之美。
作為研發(fā)人員:要時(shí)常思考面對當(dāng)前的項(xiàng)目,是否能夠簡單重構(gòu)讓程序跑的更順溜。
作為研發(fā)人員:要多看、多悟、多提煉、多實(shí)踐。
作為研發(fā)人員:請不要放棄代碼,因?yàn)槌绦蚪K會(huì)鑄就人生。
本次分享就到這里,希望對你有所幫助吧。
一起聊技術(shù)、談業(yè)務(wù)、噴架構(gòu),少走彎路,不踩大坑。歡迎關(guān)注「一猿小講」,會(huì)持續(xù)輸出原創(chuàng)精彩分享,敬請期待!