想象一下,如果你現在剛好在 word 上寫需求文檔,電腦突然重啟。等待開機完成,你可能會發現寫了一個小時文檔沒有保存,就這么沒了。。。
一個正在運行 Java 應用如果突然將其停止,影響不止數據丟失,還會造成其他影響。比如:
- 請求丟失:內存隊列中等待執行請求丟失
- 數據丟失:處於內存緩存中數據未持久化到磁盤
- 文件損壞:正在寫的文件沒有沒有更新完成,導致文件損壞
- 業務中斷:處理一半的業務被強行中斷,如支付成功了,卻沒有更新到數據庫中
- 服務未下線:上游服務依然往停止節點發送請求
所以在關閉服務之前,我們需要先做好善后工作,比如保存數據,清理資源,下線服務,然后才退出應用。這種有計划平滑的關閉應用相對直接停止應用,就顯得非常『優雅』。
ps: 仔細品味,優雅停機這個詞真好~
ShutdownHook
Java 語言提供一種 ShutdownHook(鈎子)進制,當 JVM 接受到系統的關閉通知之后,調用 ShutdownHook 內的方法,用以完成清理操作,從而平滑的退出應用。
ShutdownHook代碼如下:
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
System.out.println("關閉應用,釋放資源");
}));
Runtime.getRuntime().addShutdownHook(Thread)
需要傳入一個線程對象,后續動作將會在該異步線程內完成。除了主動關閉應用(使用 kill -15 指令),以下場景也將會觸發 ShutdownHook :
- 代碼執行結束,JVM 正常退出
- 應用代碼中調用
System#exit
方法 - 應用中發生 OOM 錯誤,導致 JVM 關閉
- 終端中使用
Ctrl+C
(非后台運行)
目前很多開源框架都是基於這個機制實現優雅停機,比如 Dubbo,Spring 等。
相關注意點
ShutdownHook 代碼實現起來相對簡單,但是我們還是需要小心下面這些坑。
Runtime.getRuntime().addShutdownHook(Thread)
可以被多次調用
我們可以多次調用 Runtime.getRuntime().addShutdownHook(Thread)
方法,從而增加多個。但是需要注意的是,多個 ShutdownHook 之間並無任何順序,Java 並不會按照加入順序執行,反而將會並發執行。
所以盡量在一個 ShutdownHook 完成所有操作。
ShutdownHook 需要盡快執行結束
不要在 ShutdownHook 執行需要被阻塞代碼,如 I/0 讀寫,這樣就會導致應用短時間不能被關閉。
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
while (true){
System.out.println("關閉應用,釋放資源");
}
}));
上面代碼中,我們使用 while(true)
模擬長時間阻塞這種極端情況,關閉該應用時,應用將會一直阻塞在 while
代碼中,導致應用沒辦法被關閉。
除了阻塞之外,還需要小心其他會讓線程阻塞的行為,比如死鎖。
為了避免 ShutdownHook 線程被長時間阻塞,我們可以引入超時進制。如果等待一定時間之后,ShutdownHook 還未完成,由腳本直接調用 kill -9 強制退出或者 ShutdownHook 代碼中引入超時進制。
文章首發於studyidea.cn/shutdownHook
歡迎關注我的公眾號:程序通事,獲得日常干貨推送。如果您對我的專題內容感興趣,也可以關注我的博客:studyidea.cn