在 JAVA 的世界里遨游,如果能擁有一雙善于發現的眼睛,有很多東西留心去看,外加耐心助力,仔細去品,往往會品出不一樣的味道。
通過本次分享,能讓你輕松 get 如下幾點,絕對收獲滿滿。
a)如何讓 Java 程序實現優雅停服?有思想才是硬道理!
b)addShutdownHook 的使用場景?會用才是王道!
c)addShutdownHook 鉤子函數到底是個啥?刨根問底!
1. 如何讓 Java 程序實現優雅停服?
無論是自研基礎服務框架,還是分析開源項目源碼,細心的 Java 開發同學,都會發現 Runtime.getRuntime().addShutdownHook 這么一句代碼的身影,這句到底是干什么用的?
接下來就一起細品,看看它香不香?
阿里開源的數據同步神器 Canal 啟動時的部分源碼:
Apache 麾下的用于海量日志收集的 Flume 啟動時的部分源碼:
仰望了一下開源的項目,不妨從中提煉一下共性(同樣的代碼遇到多次,勢必會品出味道),寫段代碼跑跑看(站在 flume 源碼的肩膀上,起飛)。
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* 體驗 Java 優雅停服
*
* @author 一猿小講
*/
public class Application {
/**
* 監控服務
*/
private ScheduledThreadPoolExecutor monitorService;
public Application() {
monitorService = new ScheduledThreadPoolExecutor(1);
}
/**
* 啟動監控服務,監控一下內存信息
*/
public void start() {
System.out.println(String.format("啟動監控服務 %s", Thread.currentThread().getId()));
monitorService.scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
System.out.println(String.format("最大內存: %dm 已分配內存: %dm 已分配內存中的剩余空間: %dm 最大可用內存: %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 源碼)
* 主要用于關閉線程池(看不懂的同學莫糾結,當做黑盒去對待)
*/
public void stop() {
System.out.println(String.format("開始關閉線程池 %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("線程池關閉完成 %s", Thread.currentThread().getId()));
}
/**
* 應用入口
*/
public static void main(String[] args) {
Application application = new Application();
// 啟動服務(每隔一段時間監控輸出一下內存信息)
application.start();
// 添加鉤子,實現優雅停服(主要驗證鉤子的作用)
final Application appReference = application;
Runtime.getRuntime().addShutdownHook(new Thread("shutdown-hook") {
@Override
public void run() {
System.out.println("接收到退出的訊號,開始打掃戰場,釋放資源,完成優雅停服");
appReference.stop();
}
});
System.out.println("服務啟動完成");
}
}
經常讀文的我很清楚,耐心讀文章中源碼的同學應該很少,所以我還是用圖給你簡單捋一捋。
標注1:start 方法利用線程池啟動一個線程去定時監控內存信息;
標注2:stop 方法用于在退出程序之前,進行關閉線程池進而釋放資源。
程序跑起來,效果如下。
當進行 kill 操作時,程序確實進行了資源釋放,效果確實很優雅。
一切看似那么自然,一切又是那么完美,這是真的嗎?殺進程時候如果用 kill -9,這種情況下會發生什么現象呢?
嗚呼!結果不會騙人的,當用 kill -9 的時候,就顯得很粗暴了,壓根不管什么資源釋放,不管三七二十一,就是終止程序。
估計很多同學,都擅長用 kill -9 進行殺進程,為了線上的應用安全,還是用 kill -15 命令殺進程吧,這樣會給應用留點時間去打掃一下戰場,釋放一下資源。
好了,通過仔細品味,借助 JDK 自帶的 addShutdownHook 來助力應用,確實能讓線上服務跑起來很優雅。
有思想才是硬道理!
2. addShutdownHook 的使用場景?
通過代碼試驗,能夠感知 addShutdownHook(new Thread(){}) 是 JVM 銷毀前要執行的一個線程,那么只要是涉及到資源回收的場景,應該都可以滿足,下面簡單列舉幾個。
a)數據同步神器 Canal 借助它,來進行關閉 socket 鏈接、釋放 canal 的工作節點、清理緩存信息等;
b)海量日志收集 Flume 借助它,來實現線程池資源關閉、工作線程停止等;
c)在應用正常退出時,執行特定的業務邏輯、關閉資源等操作。
d)在 OOM 宕機、 CTRL+C、或執行 kill pid,導致 JVM 非正常退出時,加入必要的挽救措施成為可能。
其實,在 Java 的世界里遨游,只有想不到的,沒有做不到的!
3. addShutdownHook 鉤子函數是個啥?
刨根還要問到底!
Hook 翻譯過來是「鉤子」的意思,那顧名思義就是用來掛東西的。
如圖所示,在現實生活中,要制作臘肉,首先用鉤子把肉勾住,然后掛在竹竿上,這應該是鉤子的作用。
生活如此,一切設計理念都源于生活,在 Java 的世界里,亦是如此。
如上圖 Runtime 的源碼所示,遵循 Java 的核心思想「一切皆是對象」,那么可以把 addShutdownHook 方法可以視作掛鉤子,其實稱之為鉤子函數會好一些,而現實生活中的肉就可以抽象為釋放資源的線程。
只要有這個鉤子函數,對外就提供了擴展能力,研發人員就可以往鉤子上掛各種自定義的場景實現,這種設計你細品那絕對是香!這也就是 Canal、Flume、Tomcat 等不同應用,在優雅停服時有著不同的實現的原因吧。
大白話,鉤子函數有了,想掛什么東西,根據心情自己定就好了。
再深入去刨會發現,由于底層數據結構采用 Map 來進行存儲,那么就支持研發人員掛多個 shutdownHook 的實現,又帶來了無限的可能性(又帶來了無限的「刺激」,自己好好去體會)。
好了,避免頭大,就刨到這兒吧,感興趣的可自行順著思路繼續刨下去。
4. 寄語,寫在最后
作為研發人員:要擁有一雙善于發現的眼睛,要善于發現代碼之美。
作為研發人員:要時常思考面對當前的項目,是否能夠簡單重構讓程序跑的更順溜。
作為研發人員:要多看、多悟、多提煉、多實踐。
作為研發人員:請不要放棄代碼,因為程序終會鑄就人生。
本次分享就到這里,希望對你有所幫助吧。
一起聊技術、談業務、噴架構,少走彎路,不踩大坑。歡迎關注「一猿小講」,會持續輸出原創精彩分享,敬請期待!