Java ShutDown Hook介紹和實(shí)例
# 概述:
之前有了解過Java的ShutDown Hook機(jī)制,但是因?yàn)闆]有使用場(chǎng)景也沒有深入學(xué)習(xí),最近剛好又看到ShutDown Hook的一些東西,想著學(xué)習(xí)總結(jié)一下,做下學(xué)習(xí)記錄。Java的Shutdown Hook是一種機(jī)制,允許開發(fā)者在Java虛擬機(jī)(JVM)即將關(guān)閉之前執(zhí)行一些清理或終止操作。Shutdown Hook提供了一個(gè)鉤子,允許開發(fā)者在JVM關(guān)閉時(shí)捕獲到關(guān)閉事件并執(zhí)行相應(yīng)的邏輯。以下是一些使用場(chǎng)景:
1. 資源釋放和清理:當(dāng)應(yīng)用程序結(jié)束或JVM關(guān)閉時(shí),可以使用Shutdown Hook來釋放和清理打開的文件、網(wǎng)絡(luò)連接、數(shù)據(jù)庫(kù)連接等資源。這確保資源在程序終止之前得到適當(dāng)?shù)年P(guān)閉,避免資源泄露和數(shù)據(jù)丟失。
2. 日志記錄和統(tǒng)計(jì):Shutdown Hook可以用于記錄應(yīng)用程序的關(guān)鍵統(tǒng)計(jì)信息或生成最終的日志報(bào)告。通過在JVM關(guān)閉前執(zhí)行這些操作,可以捕獲應(yīng)用程序在運(yùn)行期間的關(guān)鍵數(shù)據(jù),并生成相應(yīng)的日志記錄。
3. 緩存刷新:如果應(yīng)用程序使用了緩存機(jī)制,可以在JVM關(guān)閉前使用Shutdown Hook來刷新緩存,將緩存中的數(shù)據(jù)寫回到持久化存儲(chǔ)或其他目標(biāo)中,確保數(shù)據(jù)的持久化和一致性。
4. 任務(wù)終止和狀態(tài)保存:在某些情況下,可能需要在應(yīng)用程序終止時(shí)保存任務(wù)的當(dāng)前狀態(tài),以便在下次啟動(dòng)時(shí)恢復(fù)。通過在Shutdown Hook中執(zhí)行任務(wù)的狀態(tài)保存操作,可以將任務(wù)的狀態(tài)保存到持久化存儲(chǔ)中,并在下次啟動(dòng)時(shí)進(jìn)行恢復(fù)。
5. 線程管理:Shutdown Hook還可以用于管理和終止應(yīng)用程序中的線程。在JVM關(guān)閉前,可以使用Shutdown Hook發(fā)送終止信號(hào)給正在運(yùn)行的線程,以確保它們?cè)诮K止之前完成當(dāng)前任務(wù)并進(jìn)行清理操作。
上面說的這些使用場(chǎng)景,我都沒用到過,大家可以先了解一下對(duì)ShutDownHook有一個(gè)簡(jiǎn)單的認(rèn)識(shí)。
# 分析:
下面我們從源碼上去看一下,ShutDown Hook的方法和原理。
跟它相關(guān)的主要有兩個(gè)類ApplicationShutdownHooks和Runtime
```
class ApplicationShutdownHooks {
? ? /* The set of registered hooks */
? ? private static IdentityHashMap<Thread, Thread> hooks;
? ? static {
? ? ? ? try {
? ? ? ? ? ? Shutdown.add(1 /* shutdown hook invocation order */,
? ? ? ? ? ? ? ? false /* not registered if shutdown in progress */,
? ? ? ? ? ? ? ? new Runnable() {
? ? ? ? ? ? ? ? ? ? public void run() {
? ? ? ? ? ? ? ? ? ? ? ? runHooks();
? ? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? }
? ? ? ? ? ? );
? ? ? ? ? ? hooks = new IdentityHashMap<>();
? ? ? ? } catch (IllegalStateException e) {
? ? ? ? ? ? // application shutdown hooks cannot be added if
? ? ? ? ? ? // shutdown is in progress.
? ? ? ? ? ? hooks = null;
? ? ? ? }
? ? }
? ? private ApplicationShutdownHooks() {}
? ? /* Add a new shutdown hook.? Checks the shutdown state and the hook itself,
? ? ?* but does not do any security checks.
? ? ?*/
? ? static synchronized void add(Thread hook) {
? ? ? ? if(hooks == null)
? ? ? ? ? ? throw new IllegalStateException("Shutdown in progress");
? ? ? ? if (hook.isAlive())
? ? ? ? ? ? throw new IllegalArgumentException("Hook already running");
? ? ? ? if (hooks.containsKey(hook))
? ? ? ? ? ? throw new IllegalArgumentException("Hook previously registered");
? ? ? ? hooks.put(hook, hook);
? ? }
? ? /* Remove a previously-registered hook.? Like the add method, this method
? ? ?* does not do any security checks.
? ? ?*/
? ? static synchronized boolean remove(Thread hook) {
? ? ? ? if(hooks == null)
? ? ? ? ? ? throw new IllegalStateException("Shutdown in progress");
? ? ? ? if (hook == null)
? ? ? ? ? ? throw new NullPointerException();
? ? ? ? return hooks.remove(hook) != null;
? ? }
? ? /* Iterates over all application hooks creating a new thread for each
? ? ?* to run in. Hooks are run concurrently and this method waits for
? ? ?* them to finish.
? ? ?*/
? ? static void runHooks() {
? ? ? ? Collection<Thread> threads;
? ? ? ? synchronized(ApplicationShutdownHooks.class) {
? ? ? ? ? ? threads = hooks.keySet();
? ? ? ? ? ? hooks = null;
? ? ? ? }
? ? ? ? for (Thread hook : threads) {
? ? ? ? ? ? hook.start();
? ? ? ? }
? ? ? ? for (Thread hook : threads) {
? ? ? ? ? ? try {
? ? ? ? ? ? ? ? hook.join();
? ? ? ? ? ? } catch (InterruptedException x) { }
? ? ? ? }
? ? }
}
```
```
?-- Runtime.class里面的方法
?public void addShutdownHook(Thread hook) {
? ? ? ? SecurityManager sm = System.getSecurityManager();
? ? ? ? if (sm != null) {
? ? ? ? ? ? sm.checkPermission(new RuntimePermission("shutdownHooks"));
? ? ? ? }
? ? ? ? ApplicationShutdownHooks.add(hook);
? ? }
? ? public boolean removeShutdownHook(Thread hook) {
? ? ? ? SecurityManager sm = System.getSecurityManager();
? ? ? ? if (sm != null) {
? ? ? ? ? ? sm.checkPermission(new RuntimePermission("shutdownHooks"));
? ? ? ? }
? ? ? ? return ApplicationShutdownHooks.remove(hook);
? ? }
? ? public void halt(int status) {
? ? ? ? SecurityManager sm = System.getSecurityManager();
? ? ? ? if (sm != null) {
? ? ? ? ? ? sm.checkExit(status);
? ? ? ? }
? ? ? ? Shutdown.halt(status);
? ? }
```
結(jié)合這兩個(gè)類的源碼,我們可以看到添加Hook其實(shí)是往ApplicationShutdownHooks的靜態(tài)Map里面放入新的線程,但是這些線程只是創(chuàng)建后被保存了起來,只有當(dāng)程序退出時(shí),runHooks被執(zhí)行,每一個(gè)帶有Hook任務(wù)的線程才的start()方法才被執(zhí)行,也因?yàn)镠ook之間是相互獨(dú)立的線程,所以它們之間執(zhí)行是沒有順序的,而且因?yàn)橹骶€程調(diào)用了每個(gè)Hook的線程的join方法,所以主線程會(huì)等待Hook全部執(zhí)行完畢在退出。
##? 無法被添加的情況:
```
? ? if(hooks == null)
? ? ? ? ? ? throw new IllegalStateException("Shutdown in progress");
? ?if (hook.isAlive())
? ? ? ? throw new IllegalArgumentException("Hook already running");
? ? if (hooks.containsKey(hook))
? ? ? ? throw new IllegalArgumentException("Hook previously registered");
```
1.ApplicationShutdownHooks已經(jīng)在調(diào)用Hook時(shí),hooks會(huì)置為null,不能在添加hook
2.Hook的Thread不能是已經(jīng)在運(yùn)行狀態(tài)的線程
3.因?yàn)閮?chǔ)存的Hook是根據(jù)線程是否相同來判斷的,所以同樣的Hook無法被添加
## 不適用的情況:
1.因?yàn)镾hutDown Hook只能處理正常退出的情況,kill -9這種是無法處理的
2Shutdown.halt和kill -9一樣都是強(qiáng)制退出,不會(huì)給Hook執(zhí)行的機(jī)會(huì)
# 使用:
下面放了一些簡(jiǎn)單的測(cè)試ShutDown的小例子,[github地址](https://github.com/wxwwt/java-practice/tree/master/src/main/java/com/scott/java/task/shutdown/hook):
```
?@Test
? ? public void test1() {
? ? ? ? // 測(cè)試正常退出的情況
? ? ? ? Runtime.getRuntime().addShutdownHook(
? ? ? ? ? ? ? ? new Thread(
? ? ? ? ? ? ? ? ? ? ? ? () -> {
? ? ? ? ? ? ? ? ? ? ? ? ? ? System.out.println("hook1 執(zhí)行了");
? ? ? ? ? ? ? ? ? ? ? ? })
? ? ? ? );
? ? }
? ??
? ? 輸出:hook1 執(zhí)行了
```
```
?@Test
? ? public void test2() {
? ? ? ? // 測(cè)試Hook執(zhí)行順序是否真的無序
? ? ? ? Runtime.getRuntime().addShutdownHook(
? ? ? ? ? ? ? ? new Thread(
? ? ? ? ? ? ? ? ? ? ? ? () -> {
? ? ? ? ? ? ? ? ? ? ? ? ? ? System.out.println("hook1 執(zhí)行了");
? ? ? ? ? ? ? ? ? ? ? ? })
? ? ? ? );
? ? ? ? Runtime.getRuntime().addShutdownHook(
? ? ? ? ? ? ? ? new Thread(
? ? ? ? ? ? ? ? ? ? ? ? () -> {
? ? ? ? ? ? ? ? ? ? ? ? ? ? System.out.println("hook2 執(zhí)行了");
? ? ? ? ? ? ? ? ? ? ? ? })
? ? ? ? );
? ? }
? ??
? ? 輸出:輸出結(jié)果hook1和hook2會(huì)隨機(jī)打印,沒有固定順序
```
```
?@Test
? ? public void test3() {
? ? ? ? // 測(cè)試kill -9 會(huì)執(zhí)行Hook嗎
? ? ? ? Runtime.getRuntime().addShutdownHook(
? ? ? ? ? ? ? ? new Thread(
? ? ? ? ? ? ? ? ? ? ? ? () -> {
? ? ? ? ? ? ? ? ? ? ? ? ? ? System.out.println("hook1 執(zhí)行了");
? ? ? ? ? ? ? ? ? ? ? ? })
? ? ? ? );
? ? ? ? while(true) {
? ? ? ? }
? ? }
? ??
? ? 輸出:
```
```
? ? @Test
? ? public void test4() {
? ? ? ? // 測(cè)試oom時(shí) 會(huì)執(zhí)行Hook嗎
? ? ? ? Runtime.getRuntime().addShutdownHook(
? ? ? ? ? ? ? ? new Thread(
? ? ? ? ? ? ? ? ? ? ? ? () -> {
? ? ? ? ? ? ? ? ? ? ? ? ? ? System.out.println("hook1 執(zhí)行了");
? ? ? ? ? ? ? ? ? ? ? ? })
? ? ? ? );
? ?
? ? ? ? List<Object> list = Lists.newArrayList();
? ? ? ? while(true) {
? ? ? ? ? ?list.add(new ShutDownHookTest());
? ? ? ? }
? ? }
? ??
? ? 輸出:
? ? ? java.lang.OutOfMemoryError: GC overhead limit exceeded
? ? ? ? ?at com.scott.java.task.shutdown.hook.ShutDownHookTest.test4(ShutDownHookTest.java:74)
? ? ? ? ?。。。省略不重要的日志
? ? ? ? ?hook1 執(zhí)行了
```
```
?@Test
? ? public void test5() {
? ? ? ? // 測(cè)試移除Hook后,會(huì)執(zhí)行Hook嗎
? ? ? ? Thread thread = new Thread(() -> {
? ? ? ? ? ? System.out.println("hook1 執(zhí)行了");
? ? ? ? });
? ? ? ? Runtime.getRuntime().addShutdownHook(thread);
? ? ? ? Runtime.getRuntime().removeShutdownHook(thread);
? ? }
? ??
? ? 輸出:
```
```
? ? @Test
? ? public void test6() {
? ? ? ? // 測(cè)試執(zhí)行halt方法后,會(huì)執(zhí)行Hook嗎
? ? ? ? Thread thread = new Thread(() -> {
? ? ? ? ? ? System.out.println("hook1 執(zhí)行了");
? ? ? ? });
? ? ? ? Runtime.getRuntime().addShutdownHook(thread);
? ? ? ? Runtime.getRuntime().halt(111);
? ? }
? ? 輸出:
```
```
?@Test
? ? public void test7() {
? ? ? ? // 測(cè)試已經(jīng)執(zhí)行Hook時(shí),還能添加新的hook嗎
? ? ? ? Thread thread = new Thread(() -> {
? ? ? ? ? ? System.out.println("hook1 執(zhí)行了");
? ? ? ? ? ? Run
? ? ? ? });
? ? ? ? Runtime.getRuntime().addShutdownHook(thread);
? ? ? ? Runtime.getRuntime().halt(111);
? ? }
? ? 輸出:
? ? hook1 執(zhí)行了
Exception in thread "Thread-0" java.lang.IllegalStateException: Shutdown in progress
```
```
? @Test
? ? public void test8() {
? ? ? ? // 測(cè)試重復(fù)注冊(cè)后,會(huì)執(zhí)行Hook嗎
? ? ? ? Thread thread = new Thread(() -> {
? ? ? ? ? ? System.out.println("hook1 執(zhí)行了");
? ? ? ? });
? ? ? ? Runtime.getRuntime().addShutdownHook(thread);
? ? ? ? Runtime.getRuntime().addShutdownHook(thread);
? ? }
? ??
? ? 輸出:java.lang.IllegalArgumentException: Hook previously registered
```
```
? @Test
? ? public void test9() {
? ? ? ? // 測(cè)試重復(fù)注冊(cè)后,會(huì)執(zhí)行Hook嗎
? ? ? ? Thread thread = new Thread(() -> {
? ? ? ? ? ? System.out.println("hook1 執(zhí)行了");
? ? ? ? });
? ? ? ? thread.start();
? ? ? ? Runtime.getRuntime().addShutdownHook(thread);
? ? }
? ??
? ? 輸出:
? ? ? ? hook1 執(zhí)行了
? ? java.lang.IllegalArgumentException: Hook already running
```
# 總結(jié)
1.ShutDown的使用還是比較簡(jiǎn)單,網(wǎng)上也有分析Spring和Dubbo等開源框架的使用例子,基本上都是用于銷毀處理資源釋放的問題
2.稍微要注意的就是一些特殊情況,比如hook執(zhí)行是無序的,不能重復(fù)添加相同的hook,已經(jīng)執(zhí)行的hook不能再創(chuàng)建新的hook等
3.平時(shí)基本沒用到過ShutDown Hook,自己想到一個(gè)比較有用的場(chǎng)景就是Jvm掛了,在Hook里面給監(jiān)控程序發(fā)通知發(fā)郵件之類的,讓技術(shù)人員來處理
# 參考資料
[1.oracle官網(wǎng)資料](https://docs.oracle.com/javase/8/docs/technotes/guides/lang/hook-design.html)
[2.Java Shutdown Hook 場(chǎng)景使用和源碼分析](https://segmentfault.com/a/1190000040167517)
[3.Adding Shutdown Hooks for JVM Applications](https://www.baeldung.com/jvm-shutdown-hooks)