您好,登錄后才能下訂單哦!
小編給大家分享一下Java中Shutdown Hook怎么用,相信大部分人都還不怎么了解,因此分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后大有收獲,下面讓我們一起去了解一下吧!
如果想在 Java 進程退出時,包括正常和異常退出,做一些額外處理工作,例如資源清理,對象銷毀,內(nèi)存數(shù)據(jù)持久化到磁盤,等待線程池處理完所有任務(wù)等等。特別是進程異常掛掉的情況,如果一些重要狀態(tài)沒及時保留下來,或線程池的任務(wù)沒被處理完,有可能會造成嚴(yán)重問題。那該怎么辦呢?
Java 中的 Shutdown Hook 提供了比較好的方案。我們可以通過 Java.Runtime.addShutdownHook(Thread hook) 方法向 JVM 注冊關(guān)閉鉤子,在 JVM 退出之前會自動調(diào)用執(zhí)行鉤子方法,做一些結(jié)尾操作,從而讓進程平滑優(yōu)雅的退出,保證了業(yè)務(wù)的完整性。
其實,shutdown hook 就是一個簡單的已初始化但是未啟動的線程。當(dāng)虛擬機開始關(guān)閉時,它將會調(diào)用所有已注冊的鉤子,這些鉤子執(zhí)行是并發(fā)的,執(zhí)行順序是不確定的。
在虛擬機關(guān)閉的過程中,還可以繼續(xù)注冊新的鉤子,或者撤銷已經(jīng)注冊過的鉤子。不過有可能會拋出 IllegalStateException。注冊和注銷鉤子的方法定義如下:
public void addShutdownHook(Thread hook) { // 省略 } public void removeShutdownHook(Thread hook) { // 省略 }
關(guān)閉鉤子可以在以下幾種場景被調(diào)用:
程序正常退出
程序調(diào)用 System.exit() 退出
終端使用 Ctrl+C 中斷程序
程序拋出異常導(dǎo)致程序退出,例如 OOM,數(shù)組越界等異常
系統(tǒng)事件,例如用戶注銷或關(guān)閉系統(tǒng)
使用 Kill pid 命令殺掉進程,注意使用 kill -9 pid 強制殺掉不會觸發(fā)執(zhí)行鉤子
驗證程序正常退出情況
package com.chenpi; public class ShutdownHookDemo { static { Runtime.getRuntime().addShutdownHook(new Thread(() -> System.out.println("執(zhí)行鉤子方法..."))); } public static void main(String[] args) throws InterruptedException { System.out.println("程序開始啟動..."); Thread.sleep(2000); System.out.println("程序即將退出..."); } }
運行結(jié)果
程序開始啟動...
程序即將退出...
執(zhí)行鉤子方法...Process finished with exit code 0
驗證程序調(diào)用 System.exit() 退出情況
package com.chenpi; public class ShutdownHookDemo { static { Runtime.getRuntime().addShutdownHook(new Thread(() -> System.out.println("執(zhí)行鉤子方法..."))); } public static void main(String[] args) throws InterruptedException { System.out.println("程序開始啟動..."); Thread.sleep(2000); System.exit(-1); System.out.println("程序即將退出..."); } }
運行結(jié)果
程序開始啟動...
執(zhí)行鉤子方法...Process finished with exit code -1
驗證終端使用 Ctrl+C 中斷程序,在命令行窗口中運行程序,然后使用 Ctrl+C 中斷
package com.chenpi; public class ShutdownHookDemo { static { Runtime.getRuntime().addShutdownHook(new Thread(() -> System.out.println("執(zhí)行鉤子方法..."))); } public static void main(String[] args) throws InterruptedException { System.out.println("程序開始啟動..."); Thread.sleep(2000); System.out.println("程序即將退出..."); } }
運行結(jié)果
D:\IdeaProjects\java-demo\java ShutdownHookDemo
程序開始啟動...
執(zhí)行鉤子方法...
演示拋出異常導(dǎo)致程序異常退出
package com.chenpi; public class ShutdownHookDemo { static { Runtime.getRuntime().addShutdownHook(new Thread(() -> System.out.println("執(zhí)行鉤子方法..."))); } public static void main(String[] args) { System.out.println("程序開始啟動..."); int a = 0; System.out.println(10 / a); System.out.println("程序即將退出..."); } }
運行結(jié)果
程序開始啟動...
執(zhí)行鉤子方法...
Exception in thread "main" java.lang.ArithmeticException: / by zero
at com.chenpi.ShutdownHookDemo.main(ShutdownHookDemo.java:12)Process finished with exit code 1
至于系統(tǒng)被關(guān)閉,或者使用 Kill pid 命令殺掉進程就不演示了,感興趣的可以自行驗證。
可以向虛擬機注冊多個關(guān)閉鉤子,但是注意這些鉤子執(zhí)行是并發(fā)的,執(zhí)行順序是不確定的。
package com.chenpi; public class ShutdownHookDemo { static { Runtime.getRuntime().addShutdownHook(new Thread(() -> System.out.println("執(zhí)行鉤子方法A..."))); Runtime.getRuntime().addShutdownHook(new Thread(() -> System.out.println("執(zhí)行鉤子方法B..."))); Runtime.getRuntime().addShutdownHook(new Thread(() -> System.out.println("執(zhí)行鉤子方法C..."))); } public static void main(String[] args) throws InterruptedException { System.out.println("程序開始啟動..."); Thread.sleep(2000); System.out.println("程序即將退出..."); } }
運行結(jié)果
程序開始啟動...
程序即將退出...
執(zhí)行鉤子方法B...
執(zhí)行鉤子方法C...
執(zhí)行鉤子方法A...
向虛擬機注冊的鉤子方法需要盡快執(zhí)行結(jié)束,盡量不要執(zhí)行長時間的操作,例如 I/O 等可能被阻塞的操作,死鎖等,這樣就會導(dǎo)致程序短時間不能被關(guān)閉,甚至一直關(guān)閉不了。我們也可以引入超時機制強制退出鉤子,讓程序正常結(jié)束。
package com.chenpi; public class ShutdownHookDemo { static { Runtime.getRuntime().addShutdownHook(new Thread(() -> { // 模擬長時間的操作 try { Thread.sleep(1000000); } catch (InterruptedException e) { e.printStackTrace(); } })); } public static void main(String[] args) throws InterruptedException { System.out.println("程序開始啟動..."); Thread.sleep(2000); System.out.println("程序即將退出..."); } }
以上的鉤子執(zhí)行時間比較長,最終會導(dǎo)致程序在等待很長時間之后才能被關(guān)閉。
如果 JVM 已經(jīng)調(diào)用執(zhí)行關(guān)閉鉤子的過程中,不允許注冊新的鉤子和注銷已經(jīng)注冊的鉤子,否則會報 IllegalStateException 異常。通過源碼分析,JVM 調(diào)用鉤子的時候,即調(diào)用 ApplicationShutdownHooks#runHooks() 方法,會將所有鉤子從變量 hooks 取出,然后將此變量置為 null。
// 調(diào)用執(zhí)行鉤子 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) { } } }
在注冊和注銷鉤子的方法中,首先會判斷 hooks 變量是否為 null,如果為 null 則拋出異常。
// 注冊鉤子 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); } // 注銷鉤子 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; }
我們演示下這種情況
package com.chenpi; public class ShutdownHookDemo { static { Runtime.getRuntime().addShutdownHook(new Thread(() -> { System.out.println("執(zhí)行鉤子方法..."); Runtime.getRuntime().addShutdownHook(new Thread( () -> System.out.println("在JVM調(diào)用鉤子的過程中再新注冊鉤子,會報錯IllegalStateException"))); // 在JVM調(diào)用鉤子的過程中注銷鉤子,會報錯IllegalStateException Runtime.getRuntime().removeShutdownHook(Thread.currentThread()); })); } public static void main(String[] args) throws InterruptedException { System.out.println("程序開始啟動..."); Thread.sleep(2000); System.out.println("程序即將退出..."); } }
運行結(jié)果
程序開始啟動...
程序即將退出...
執(zhí)行鉤子方法...
Exception in thread "Thread-0" java.lang.IllegalStateException: Shutdown in progress
at java.lang.ApplicationShutdownHooks.add(ApplicationShutdownHooks.java:66)
at java.lang.Runtime.addShutdownHook(Runtime.java:211)
at com.chenpi.ShutdownHookDemo.lambda$static$1(ShutdownHookDemo.java:8)
at java.lang.Thread.run(Thread.java:748)
如果調(diào)用 Runtime.getRuntime().halt() 方法停止 JVM,那么虛擬機是不會調(diào)用鉤子的。
package com.chenpi; public class ShutdownHookDemo { static { Runtime.getRuntime().addShutdownHook(new Thread(() -> System.out.println("執(zhí)行鉤子方法..."))); } public static void main(String[] args) { System.out.println("程序開始啟動..."); System.out.println("程序即將退出..."); Runtime.getRuntime().halt(0); } }
運行結(jié)果
程序開始啟動...
程序即將退出...Process finished with exit code 0
如果要想終止執(zhí)行中的鉤子方法,只能通過調(diào)用 Runtime.getRuntime().halt() 方法,強制讓程序退出。在Linux環(huán)境中使用 kill -9 pid 命令也是可以強制終止退出。
package com.chenpi; public class ShutdownHookDemo { static { Runtime.getRuntime().addShutdownHook(new Thread(() -> { System.out.println("開始執(zhí)行鉤子方法..."); Runtime.getRuntime().halt(-1); System.out.println("結(jié)束執(zhí)行鉤子方法..."); })); } public static void main(String[] args) { System.out.println("程序開始啟動..."); System.out.println("程序即將退出..."); } }
運行結(jié)果
程序開始啟動...
程序即將退出...
開始執(zhí)行鉤子方法...Process finished with exit code -1
如果程序使用 Java Security Managers,使用 shutdown Hook 則需要安全權(quán)限 RuntimePermission(“shutdownHooks”),否則會導(dǎo)致 SecurityException。
例如,我們程序自定義了一個線程池,用來接收和處理任務(wù)。如果程序突然奔潰異常退出,這時線程池的所有任務(wù)有可能還未處理完成,如果不處理完程序就直接退出,可能會導(dǎo)致數(shù)據(jù)丟失,業(yè)務(wù)異常等重要問題。這時鉤子就派上用場了。
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; public class ShutdownHookDemo { // 線程池 private static ExecutorService executorService = Executors.newFixedThreadPool(3); static { Runtime.getRuntime().addShutdownHook(new Thread(() -> { System.out.println("開始執(zhí)行鉤子方法..."); // 關(guān)閉線程池 executorService.shutdown(); try { // 等待60秒 System.out.println(executorService.awaitTermination(60, TimeUnit.SECONDS)); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("結(jié)束執(zhí)行鉤子方法..."); })); } public static void main(String[] args) throws InterruptedException { System.out.println("程序開始啟動..."); // 向線程池添加10個任務(wù) for (int i = 0; i < 10; i++) { Thread.sleep(1000); final int finalI = i; executorService.execute(() -> { try { Thread.sleep(4000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Task " + finalI + " execute..."); }); System.out.println("Task " + finalI + " is in thread pool..."); } } }
在命令行窗口中運行程序,在10個任務(wù)都提交到線程池之后,任務(wù)都還未處理完成之前,使用 Ctrl+C 中斷程序,最終在虛擬機關(guān)閉之前,調(diào)用了關(guān)閉鉤子,關(guān)閉線程池,并且等待60秒讓所有任務(wù)執(zhí)行完成。
Shutdown Hook 在 Spring 中是如何運用的呢。通過源碼分析,Springboot 項目啟動時會判斷 registerShutdownHook 的值是否為 true,默認是 true,如果為真則向虛擬機注冊關(guān)閉鉤子。
private void refreshContext(ConfigurableApplicationContext context) { refresh(context); if (this.registerShutdownHook) { try { context.registerShutdownHook(); } catch (AccessControlException ex) { // Not allowed in some environments. } } } @Override public void registerShutdownHook() { if (this.shutdownHook == null) { // No shutdown hook registered yet. this.shutdownHook = new Thread() { @Override public void run() { synchronized (startupShutdownMonitor) { // 鉤子方法 doClose(); } } }; // 底層還是使用此方法注冊鉤子 Runtime.getRuntime().addShutdownHook(this.shutdownHook); } }
在關(guān)閉鉤子的方法 doClose 中,會做一些虛擬機關(guān)閉前處理工作,例如銷毀容器里所有單例 Bean,關(guān)閉 BeanFactory,發(fā)布關(guān)閉事件等等。
protected void doClose() { // Check whether an actual close attempt is necessary... if (this.active.get() && this.closed.compareAndSet(false, true)) { if (logger.isDebugEnabled()) { logger.debug("Closing " + this); } LiveBeansView.unregisterApplicationContext(this); try { // 發(fā)布Spring 應(yīng)用上下文的關(guān)閉事件,讓監(jiān)聽器在應(yīng)用關(guān)閉之前做出響應(yīng)處理 publishEvent(new ContextClosedEvent(this)); } catch (Throwable ex) { logger.warn("Exception thrown from ApplicationListener handling ContextClosedEvent", ex); } // Stop all Lifecycle beans, to avoid delays during individual destruction. if (this.lifecycleProcessor != null) { try { // 執(zhí)行l(wèi)ifecycleProcessor的關(guān)閉方法 this.lifecycleProcessor.onClose(); } catch (Throwable ex) { logger.warn("Exception thrown from LifecycleProcessor on context close", ex); } } // 銷毀容器里所有單例Bean destroyBeans(); // 關(guān)閉BeanFactory closeBeanFactory(); // Let subclasses do some final clean-up if they wish... onClose(); // Reset local application listeners to pre-refresh state. if (this.earlyApplicationListeners != null) { this.applicationListeners.clear(); this.applicationListeners.addAll(this.earlyApplicationListeners); } // Switch to inactive. this.active.set(false); } }
我們知道,我們可以定義 bean 并且實現(xiàn) DisposableBean 接口,重寫 destroy 對象銷毀方法。destroy 方法就是在 Spring 注冊的關(guān)閉鉤子里被調(diào)用的。例如我們使用 Spring 框架的 ThreadPoolTaskExecutor 線程池類,它就實現(xiàn)了 DisposableBean 接口,重寫了 destroy 方法,從而在程序退出前,進行線程池銷毀工作。源碼如下:
@Override public void destroy() { shutdown(); } /** * Perform a shutdown on the underlying ExecutorService. * @see java.util.concurrent.ExecutorService#shutdown() * @see java.util.concurrent.ExecutorService#shutdownNow() */ public void shutdown() { if (logger.isInfoEnabled()) { logger.info("Shutting down ExecutorService" + (this.beanName != null ? " '" + this.beanName + "'" : "")); } if (this.executor != null) { if (this.waitForTasksToCompleteOnShutdown) { this.executor.shutdown(); } else { for (Runnable remainingTask : this.executor.shutdownNow()) { cancelRemainingTask(remainingTask); } } awaitTerminationIfNecessary(this.executor); } }
以上是“Java中Shutdown Hook怎么用”這篇文章的所有內(nèi)容,感謝各位的閱讀!相信大家都有了一定的了解,希望分享的內(nèi)容對大家有所幫助,如果還想學(xué)習(xí)更多知識,歡迎關(guān)注億速云行業(yè)資訊頻道!
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。