
精品專欄
優雅停機? 這個名詞我是服的,如果拋開專業不談,多好的名詞啊!
其實優雅停機,就是在要關閉服務之前,不是立馬全部關停,而是做好一些善後操作,比如:關閉執行緒、釋放連線資源等。
再比如,就是不會讓呼叫方的請求處理了一增,一下就中斷了。而處理完本次後,再停止服務。
Java語言中,我們可以透過Runtime.getRuntime().addShutdownHook()方法來註冊鉤子,以保證程式平滑退出。(其他語言也類似)
來個慄子:
public class ShutdownGracefulTest {/*** 使用執行緒池處理任務*/public static ExecutorService executorService = Executors.newCachedThreadPool();public static void main(String[] args) {//假設有5個執行緒需要執行任務for(int i = 0; i < 5; i++){final int id = i;Thread taski = new Thread(new Runnable() {@Overridepublic void run() {System.out.println(System.currentTimeMillis() + " : thread_" + id + " start...");try {TimeUnit.SECONDS.sleep(id);}catch (InterruptedException e) {e.printStackTrace();}System.out.println(System.currentTimeMillis() + " : thread_" + id + " finish!");}});taski.setDaemon(true);executorService.submit(taski);}Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {@Overridepublic void run() {System.out.println(System.currentTimeMillis() + " : " + Thread.currentThread().getName() + " No1 shutdown hooking...");boolean shutdown = true;try {executorService.shutdown();System.out.println(System.currentTimeMillis() + " : " + Thread.currentThread().getName() + " shutdown signal got, wait threadPool finish.");executorService.awaitTermination(1500, TimeUnit.SECONDS);boolean done = false;System.out.println(System.currentTimeMillis() + " : " + Thread.currentThread().getName() + " all thread's done.");}catch (InterruptedException e) {e.printStackTrace();// 嘗試再次關閉if(!executorService.isTerminated()) {executorService.shutdownNow();}}System.out.println(System.currentTimeMillis() + " : " + Thread.currentThread().getName() + " No1 shutdown done...");}}));Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {@Overridepublic void run() {try {System.out.println(System.currentTimeMillis() + " : " + Thread.currentThread().getName() + " No2 shutdown hooking...");Thread.sleep(1000);}catch (InterruptedException e) {e.printStackTrace();}System.out.println(System.currentTimeMillis() + " : " + Thread.currentThread().getName() + " No2 shutdown done...");}}));System.out.println("main method exit...");System.exit(0);}}
執行結果如下:

很明顯,確實是優雅了,雖然最後收到了一關閉訊號,但是仍然保證了任務的處理完成。很棒吧!
那麼,在實際應用中是如何體現優雅停機呢?
kill -15 pid
透過該命令傳送一個關閉訊號給到jvm, 然後就開始執行 Shutdown Hook 了,你可以做很多:
- 關閉 socket 連結
- 清理臨時檔案
- 傳送訊息通知給訂閱方,告知自己下線
- 將自己將要被銷毀的訊息通知給子行程
- 各種資源的釋放 …
而在平時工作中,我們不乏看到很多運維同學,是這麼乾的:
kill -9 pid
如果這麼乾的話,jvm也無法了,kill -9 相當於一次系統宕機,系統斷電。這會給應用殺了個措手不及,沒有留給應用任何反應的機會。
所以,無論如何是優雅不起來了。
要優雅,是程式碼和運維的結合!
其中,執行緒池的關閉方式為:
executorService.shutdown();executorService.awaitTermination(1500, TimeUnit.SECONDS);
ThreadPoolExecutor 在 shutdown 之後會變成 SHUTDOWN 狀態,無法接受新的任務,隨後等待正在執行的任務執行完成。意味著,shutdown 只是發出一個命令,至於有沒有關閉還是得看執行緒自己。
ThreadPoolExecutor 對於 shutdownNow 的處理則不太一樣,方法執行之後變成 STOP 狀態,並對執行中的執行緒呼叫 Thread.interrupt() 方法(但如果執行緒未處理中斷,則不會有任何事發生),所以並不代表“立刻關閉”。
- shutdown() :啟動順序關閉,其中執行先前提交的任務,但不接受新任務。如果已經關閉,則呼叫沒有附加效果。此方法不等待先前提交的任務完成執行。
- shutdownNow():嘗試停止所有正在執行的任務,停止等待任務的處理,並傳回正在等待執行的任務的串列。當從此方法傳回時,這些任務將從任務佇列中耗盡(刪除)。此方法不等待主動執行的任務終止。
- executor.awaitTermination(this.awaitTerminationSeconds, TimeUnit.SECONDS)); 控制等待的時間,防止任務無限期的執行(前面已經強調過了,即使是 shutdownNow 也不能保證執行緒一定停止執行)。
註意:
- 虛擬機器會對多個shutdownhook以未知的順序呼叫,都執行完後再退出。
- 如果接收到 kill -15 pid 命令時,執行阻塞操作,可以做到等待任務執行完成之後再關閉 JVM。同時,也解釋了一些應用執行 kill -15 pid 無法退出的問題,如:中斷被阻塞了,或者hook運行了死迴圈程式碼。
實現原理:
Runtime.getRuntime().addShutdownHook(hook); // 新增鉤子,開啟優雅之路
// 具體流程如下:
/*** Registers a new virtual-machine shutdown hook.** @param hook* An initialized but unstarted {@link Thread} object** @throws IllegalArgumentException* If the specified hook has already been registered,* or if it can be determined that the hook is already running or* has already been run** @throws IllegalStateException* If the virtual machine is already in the process* of shutting down** @throws SecurityException* If a security manager is present and it denies* {@link RuntimePermission}("shutdownHooks")** @see #removeShutdownHook* @see #halt(int)* @see #exit(int)* @since 1.3*/public void addShutdownHook(Thread hook) {SecurityManager sm = System.getSecurityManager();if (sm != null) {sm.checkPermission(new RuntimePermission("shutdownHooks"));}// 新增到 application 中ApplicationShutdownHooks.add(hook);}// java.lang.ApplicationShutdownHooks.add(hook);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 以map型別儲存, k->k 形式儲存,保證每一個鉤子都是獨立的hooks.put(hook, hook);}// java.lang.ApplicationShutdownHooks 會先註冊一個靜態塊,新增一個任務到 Shutdown 中/* 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() {// 即當該任務被呼叫時,呼叫自身的執行方法,使所有註冊的 hook 執行起來runHooks();}});hooks = new IdentityHashMap<>();} catch (IllegalStateException e) {// application shutdown hooks cannot be added if// shutdown is in progress.hooks = null;}}// runHooks 執行所有鉤子執行緒,進行非同步呼叫/* 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) { }}}
到現在為止,我們已經知道關閉鉤子是如何執行的,但是,還不是知道,該鉤子是何時觸發?
// java.lang.Shutdown.add() 該方法會jvm主動呼叫,從而觸發 後續鉤子執行/* Invoked by the JNI DestroyJavaVM procedure when the last non-daemon* thread has finished. Unlike the exit method, this method does not* actually halt the VM.*/static void shutdown() {synchronized (lock) {switch (state) {case RUNNING: /* Initiate shutdown */state = HOOKS;break;case HOOKS: /* Stall and then return */case FINALIZERS:break;}}synchronized (Shutdown.class) {// 執行序列sequence();}}// 而 sequence() 則會呼叫 runHooks(), 呼叫自定義的鉤子任務private static void sequence() {synchronized (lock) {/* Guard against the possibility of a daemon thread invoking exit* after DestroyJavaVM initiates the shutdown sequence*/if (state != HOOKS) return;}runHooks();boolean rfoe;synchronized (lock) {state = FINALIZERS;rfoe = runFinalizersOnExit;}if (rfoe) runAllFinalizers();}// 執行鉤子,此處最多允許註冊 10 個鉤子,且進行同步呼叫,當然這是最頂級的鉤子,鉤子下還可以新增鉤子,可以任意新增n個private static void runHooks() {for (int i=0; i < MAX_SYSTEM_HOOKS; i++) {try {Runnable hook;synchronized (lock) {// acquire the lock to make sure the hook registered during// shutdown is visible here.currentRunningHook = i;hook = hooks[i];}// 同步呼叫註冊的hook, 即 前面看到 ApplicationShutdownHooks.runHooks()if (hook != null) hook.run();} catch(Throwable t) {if (t instanceof ThreadDeath) {ThreadDeath td = (ThreadDeath)t;throw td;}}}}
如此,整個關閉流程完美了。
簡化為:
- 註冊流程(應用主動呼叫):Runtime.addShutdownHook -> ApplicationShutdownHooks.add()/static -> java.lang.Shutdown.add()/shutdown()
- 執行流程(jvm自動呼叫):java.lang.Shutdown.shutdown()->sequence()->runHooks() -> ApplicationShutdownHooks.runHooks() -> hooks 最終
出處:https://www.cnblogs.com/yougewe/p/9881874.html
多執行緒:為什麼在while迴圈中加入System.out.println,執行緒可以停止
END

>>>>>> 加群交流技術 <<<<<<
知識星球
