在 Java 的世界裡遨遊,如果能擁有一雙善於發現的眼睛,有很多東西留心去看,外加耐心助力,仔細去品,往往會品出不一樣的味道。
通過本次分享,能讓你輕鬆 get 如下幾點,絕對收穫滿滿。
a)如何讓 Java 程式實現優雅停服?有思想才是硬道理!
b)addShutdownHook 的使用場景?會用才是王道!
c)addShutdownHook 鉤子函式到底是個啥?刨根問底!
1. 如何讓 Java 程式實現優雅停服?
無論是自研基礎服務框架,還是分析開源專案原始碼,細心的 Java 開發同學,都會發現 Runtime.getRuntime().addShutdownHook 這麼一句程式碼的身影,這句到底是幹什麼用的?
接下來就一起細品,看看它香不香?
阿里開源的資料同步神器 Canal 啟動時的部分原始碼:
Apache 麾下的用於海量日誌收集的 Flume 啟動時的部分原始碼:
仰望了一下開源的專案,不妨從中提煉一下共性(同樣的程式碼遇到多次,勢必會品出味道),寫段程式碼跑跑看(站在 flume 原始碼的肩膀上,起飛)。
1 import java.util.concurrent.ScheduledThreadPoolExecutor; 2 import java.util.concurrent.TimeUnit; 3 4 /** 5 * 體驗 Java 優雅停服 6 * 7 * @author 一猿小講 8 */ 9 public class Application { 10 11 /** 12 * 監控服務 13 */ 14 private ScheduledThreadPoolExecutor monitorService; 15 16 public Application() { 17 monitorService = new ScheduledThreadPoolExecutor(1); 18 } 19 20 /** 21 * 啟動監控服務,監控一下記憶體資訊 22 */ 23 public void start() { 24 System.out.println(String.format("啟動監控服務 %s", Thread.currentThread().getId())); 25 monitorService.scheduleWithFixedDelay(new Runnable() { 26 @Override 27 public void run() { 28 System.out.println(String.format("最大記憶體: %dm 已分配記憶體: %dm 已分配記憶體中的剩餘空間: %dm 最大可用記憶體: %dm", 29 Runtime.getRuntime().maxMemory() / 1024 / 1024, 30 Runtime.getRuntime().totalMemory() / 1024 / 1024, 31 Runtime.getRuntime().freeMemory() / 1024 / 1024, 32 (Runtime.getRuntime().maxMemory() - Runtime.getRuntime().totalMemory() + 33 Runtime.getRuntime().freeMemory()) / 1024 / 1024)); 34 } 35 }, 2, 2, TimeUnit.SECONDS); 36 } 37 38 /** 39 * 釋放資源(程式碼來源於 flume 原始碼) 40 * 主要用於關閉執行緒池(看不懂的同學莫糾結,當做黑盒去對待) 41 */ 42 public void stop() { 43 System.out.println(String.format("開始關閉執行緒池 %s", Thread.currentThread().getId())); 44 if (monitorService != null) { 45 monitorService.shutdown(); 46 try { 47 monitorService.awaitTermination(10, TimeUnit.SECONDS); 48 } catch (InterruptedException e) { 49 System.err.println("Interrupted while waiting for monitor service to stop"); 50 } 51 if (!monitorService.isTerminated()) { 52 monitorService.shutdownNow(); 53 try { 54 while (!monitorService.isTerminated()) { 55 monitorService.awaitTermination(10, TimeUnit.SECONDS); 56 } 57 } catch (InterruptedException e) { 58 System.err.println("Interrupted while waiting for monitor service to stop"); 59 } 60 } 61 } 62 System.out.println(String.format("執行緒池關閉完成 %s", Thread.currentThread().getId())); 63 } 64 65 /** 66 * 應用入口 67 */ 68 public static void main(String[] args) { 69 Application application = new Application(); 70 // 啟動服務(每隔一段時間監控輸出一下記憶體資訊) 71 application.start(); 72 73 // 新增鉤子,實現優雅停服(主要驗證鉤子的作用) 74 final Application appReference = application; 75 Runtime.getRuntime().addShutdownHook(new Thread("shutdown-hook") { 76 @Override 77 public void run() { 78 System.out.println("接收到退出的訊號,開始打掃戰場,釋放資源,完成優雅停服"); 79 appReference.stop(); 80 } 81 }); 82 System.out.println("服務啟動完成"); 83 } 84 }
經常讀文的我很清楚,耐心讀文章中原始碼的同學應該很少,所以我還是用圖給你簡單捋一捋。
標註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. 寄語,寫在最後
作為研發人員:要擁有一雙善於發現的眼睛,要善於發現程式碼之美。
作為研發人員:要時常思考面對當前的專案,是否能夠簡單重構讓程式跑的更順溜。
作為研發人員:要多看、多悟、多提煉、多實踐。
作為研發人員:請不要放棄程式碼,因為程式終會鑄就人生。
本次分享就到這裡,希望對你有所幫助吧。一起聊技術、談業務、噴架構,少走彎路,不踩大坑。
會持續輸出原創精彩分享,敬請期待!關注同名公眾號:一猿小講,回覆「1024」可以獲取精心為您準備的職場打怪進階資料。