一、問題引入
背景
在編寫一個需要持續在後臺執行的程式的時候遇到了這樣的場景:我的程式在主函式中建立了一個執行緒池週期性地執行任務,我希望主執行緒和執行緒池都持續執行,但如果收到外部的關閉訊號時,主執行緒和執行緒池也都能同時退出。想到的就是程式結束的時候需要有一個stop()方法去手動關閉執行緒池,但是怎麼控制這個stop()方法在我想要的時候呼叫,以什麼形式去接收外部的關閉訊號也成了需要考慮的問題。
原始思路
最開始的嘗試是我將程式的執行和停止分別用"start"和"stop"兩種狀態表示,然後用一個狀態檔案state去記錄當前的狀態(程式啟動時預設是"start"),如果想要關閉這個正在執行的程式,就去修改狀態檔案state,將裡面內容變為"stop"。同時在主函式中開啟這個狀態檔案,迴圈監聽裡面的內容,如果發現變為"stop",就去呼叫stop()方法執行關閉邏輯。按照這個思路,我寫了一個簡單的程式在IDEA中測試了一下效果,發現是可行的。但是當我將程式打包,在mac系統上執行jar包進行測試的時候,不知什麼原因,程式總是讀到state檔案剛開啟時的內容,不能檢測到state檔案的變化,無法按我設想的方式進行關閉。因此只能另想辦法。
無意間看見JVM鉤子函式的介紹,發現這可能正是我想要的,於是趕緊拿來試一試。
二、JVM鉤子使用場景
JVM關閉的情況如下圖所示分為三類,第一種是正常的關閉,第二種是異常關閉的情況,第三種是強制關閉的情況。
JVM鉤子函式對於前兩種方式都可以進行優雅的關閉,但是對最後一種強制關閉就不起作用了。
下面我會根據這三種JVM關閉過程進行簡單演示。
正常關閉
程式碼如下:
public class TestJVMHook {
public static void main(String[] args) {
Runtime.getRuntime().addShutdownHook(new Thread(()->
stop()
));
start();
System.out.println("===程式正常結束===");
}
public static void start() {
System.out.println("===呼叫start()方法===");
}
public static void stop() {
System.out.println("===呼叫stop()方法===");
}
}
執行結果:
===呼叫start()方法===
===程式正常結束===
===呼叫stop()方法===
可以看到,在鉤子函式中宣告瞭stop()方法,然後程式正常結束後會自動呼叫鉤子函式。
異常關閉
異常關閉分為OOM和RuntimeException兩種情況,我用除數為0的執行時異常來演示。
程式碼如下:
public class TestJVMHook {
public static void main(String[] args) {
Runtime.getRuntime().addShutdownHook(new Thread(()->
stop()
));
start();
int res = 10/0;
System.out.println("===程式結束===");
}
public static void start() {
System.out.println("===呼叫start()方法===");
}
public static void stop() {
System.out.println("===呼叫stop()方法===");
}
}
執行結果:
===呼叫start()方法===
===呼叫stop()方法===
Exception in thread "main" java.lang.ArithmeticException: / by zero
at com.example.TestJVMHook.main(TestJVMHook.java:9)
可以看到執行"10/0"時發生執行時異常,並不會正常列印下一行語句,但仍然會自動呼叫鉤子函式中stop()方法。
強制關閉
這裡我們啟動一個迴圈程式,然後手動去關閉它。
程式碼如下:
public class TestJVMHook {
public static void main(String[] args) throws InterruptedException {
Runtime.getRuntime().addShutdownHook(new Thread(()->
stop()
));
int count = 1;
start();
while(true){
System.out.println("迴圈計數器"+(count++));
Thread.sleep(5*1000);
}
}
public static void start() {
System.out.println("===呼叫start()方法===");
}
public static void stop() {
System.out.println("===呼叫stop()方法===");
}
}
啟動後檢視程式id,然後通過"kill -9 <pid>"
強制關閉:
執行結果:
還是上面那段程式碼,再次啟動,採用"kill <pid>"
關閉:
發現通過"kill
三、迴歸問題
經過一系列測試,驗證了JVM鉤子函式確實可以實現我想要的資源關閉效果。由於我的程式是一個迴圈程式,需要手動關閉,因此可以在關閉程式的指令碼中通過kill