歡迎光臨
每天分享高質量文章

哦,這就是java的優雅停機?(實現及原理)

精品專欄

 

優雅停機? 這個名詞我是服的,如果拋開專業不談,多好的名詞啊!

其實優雅停機,就是在要關閉服務之前,不是立馬全部關停,而是做好一些善後操作,比如:關閉執行緒、釋放連線資源等。

再比如,就是不會讓呼叫方的請求處理了一增,一下就中斷了。而處理完本次後,再停止服務。

Java語言中,我們可以透過Runtime.getRuntime().addShutdownHook()方法來註冊鉤子,以保證程式平滑退出。(其他語言也類似)

來個慄子:

  1. public class ShutdownGracefulTest {
  2.    /**
  3.     * 使用執行緒池處理任務
  4.     */
  5.    public static ExecutorService executorService = Executors.newCachedThreadPool();
  6.    public static void main(String[] args) {
  7.        //假設有5個執行緒需要執行任務
  8.        for(int i = 0; i < 5; i++){
  9.            final int id = i;
  10.            Thread taski = new Thread(new Runnable() {
  11.                @Override
  12.                public void run() {
  13.                    System.out.println(System.currentTimeMillis() + " : thread_" + id + " start...");
  14.                    try {
  15.                        TimeUnit.SECONDS.sleep(id);
  16.                    }
  17.                    catch (InterruptedException e) {
  18.                        e.printStackTrace();
  19.                    }
  20.                    System.out.println(System.currentTimeMillis() + " : thread_" + id + " finish!");
  21.                }
  22.            });
  23.            taski.setDaemon(true);
  24.            executorService.submit(taski);
  25.        }
  26.        Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
  27.            @Override
  28.            public void run() {
  29.                System.out.println(System.currentTimeMillis() + " : " + Thread.currentThread().getName() + " No1 shutdown hooking...");
  30.                boolean shutdown = true;
  31.                try {
  32.                    executorService.shutdown();
  33.                    System.out.println(System.currentTimeMillis() + " : " + Thread.currentThread().getName() +  " shutdown signal got, wait threadPool finish.");
  34.                    executorService.awaitTermination(1500, TimeUnit.SECONDS);
  35.                    boolean done = false;
  36.                    System.out.println(System.currentTimeMillis() + " : " + Thread.currentThread().getName() +  " all thread's done.");
  37.                }
  38.                catch (InterruptedException e) {
  39.                    e.printStackTrace();
  40.                    // 嘗試再次關閉
  41.                    if(!executorService.isTerminated()) {
  42.                        executorService.shutdownNow();
  43.                    }
  44.                }
  45.                System.out.println(System.currentTimeMillis() + " : " + Thread.currentThread().getName() + " No1 shutdown done...");
  46.            }
  47.        }));
  48.        Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
  49.            @Override
  50.            public void run() {
  51.                try {
  52.                    System.out.println(System.currentTimeMillis() + " : " + Thread.currentThread().getName() + " No2 shutdown hooking...");
  53.                    Thread.sleep(1000);
  54.                }
  55.                catch (InterruptedException e) {
  56.                    e.printStackTrace();
  57.                }
  58.                System.out.println(System.currentTimeMillis() + " : " + Thread.currentThread().getName() + " No2 shutdown done...");
  59.            }
  60.        }));
  61.        System.out.println("main method exit...");
  62.        System.exit(0);
  63.    }
  64. }

執行結果如下:

很明顯,確實是優雅了,雖然最後收到了一關閉訊號,但是仍然保證了任務的處理完成。很棒吧!

 那麼,在實際應用中是如何體現優雅停機呢?

kill -15 pid

透過該命令傳送一個關閉訊號給到jvm, 然後就開始執行 Shutdown Hook 了,你可以做很多:

  1. 關閉 socket 連結
  2. 清理臨時檔案
  3. 傳送訊息通知給訂閱方,告知自己下線
  4. 將自己將要被銷毀的訊息通知給子行程
  5. 各種資源的釋放 …

而在平時工作中,我們不乏看到很多運維同學,是這麼乾的:

kill -9 pid

如果這麼乾的話,jvm也無法了,kill -9 相當於一次系統宕機,系統斷電。這會給應用殺了個措手不及,沒有留給應用任何反應的機會。

所以,無論如何是優雅不起來了。

要優雅,是程式碼和運維的結合!

其中,執行緒池的關閉方式為:

  1. executorService.shutdown();
  2. 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); // 新增鉤子,開啟優雅之路

// 具體流程如下:

  1.    /**
  2.     * Registers a new virtual-machine shutdown hook.
  3.     *
  4.     * @param   hook
  5.     *          An initialized but unstarted {@link Thread} object
  6.     *
  7.     * @throws  IllegalArgumentException
  8.     *          If the specified hook has already been registered,
  9.     *          or if it can be determined that the hook is already running or
  10.     *          has already been run
  11.     *
  12.     * @throws  IllegalStateException
  13.     *          If the virtual machine is already in the process
  14.     *          of shutting down
  15.     *
  16.     * @throws  SecurityException
  17.     *          If a security manager is present and it denies
  18.     *          {@link RuntimePermission}("shutdownHooks")
  19.     *
  20.     * @see #removeShutdownHook
  21.     * @see #halt(int)
  22.     * @see #exit(int)
  23.     * @since 1.3
  24.     */
  25.    public void addShutdownHook(Thread hook) {
  26.        SecurityManager sm = System.getSecurityManager();
  27.        if (sm != null) {
  28.            sm.checkPermission(new RuntimePermission("shutdownHooks"));
  29.        }
  30.        // 新增到 application 中
  31.        ApplicationShutdownHooks.add(hook);
  32.    }
  33.    // java.lang.ApplicationShutdownHooks.add(hook);
  34.    static synchronized void add(Thread hook) {
  35.        if(hooks == null)
  36.            throw new IllegalStateException("Shutdown in progress");
  37.        if (hook.isAlive())
  38.            throw new IllegalArgumentException("Hook already running");
  39.        if (hooks.containsKey(hook))
  40.            throw new IllegalArgumentException("Hook previously registered");
  41.        // hooks 以map型別儲存, k->k 形式儲存,保證每一個鉤子都是獨立的
  42.        hooks.put(hook, hook);
  43.    }
  44.    // java.lang.ApplicationShutdownHooks 會先註冊一個靜態塊,新增一個任務到 Shutdown 中
  45.    /* The set of registered hooks */
  46.    private static IdentityHashMap<Thread, Thread> hooks;
  47.    static {
  48.        try {
  49.            Shutdown.add(1 /* shutdown hook invocation order */,
  50.                false /* not registered if shutdown in progress */,
  51.                new Runnable() {
  52.                    public void run() {
  53.                        // 即當該任務被呼叫時,呼叫自身的執行方法,使所有註冊的 hook 執行起來
  54.                        runHooks();
  55.                    }
  56.                }
  57.            );
  58.            hooks = new IdentityHashMap<>();
  59.        } catch (IllegalStateException e) {
  60.            // application shutdown hooks cannot be added if
  61.            // shutdown is in progress.
  62.            hooks = null;
  63.        }
  64.    }
  65.    // runHooks 執行所有鉤子執行緒,進行非同步呼叫
  66.    /* Iterates over all application hooks creating a new thread for each
  67.     * to run in. Hooks are run concurrently and this method waits for
  68.     * them to finish.
  69.     */
  70.    static void runHooks() {
  71.        Collection<Thread> threads;
  72.        synchronized(ApplicationShutdownHooks.class) {
  73.            threads = hooks.keySet();
  74.            hooks = null;
  75.        }
  76.        for (Thread hook : threads) {
  77.            hook.start();
  78.        }
  79.        for (Thread hook : threads) {
  80.            try {
  81.                // 阻塞等待所有完成
  82.                hook.join();
  83.            } catch (InterruptedException x) { }
  84.        }
  85.    }

到現在為止,我們已經知道關閉鉤子是如何執行的,但是,還不是知道,該鉤子是何時觸發?

  1.    // java.lang.Shutdown.add() 該方法會jvm主動呼叫,從而觸發 後續鉤子執行
  2.    /* Invoked by the JNI DestroyJavaVM procedure when the last non-daemon
  3.     * thread has finished.  Unlike the exit method, this method does not
  4.     * actually halt the VM.
  5.     */
  6.    static void shutdown() {
  7.        synchronized (lock) {
  8.            switch (state) {
  9.            case RUNNING:       /* Initiate shutdown */
  10.                state = HOOKS;
  11.                break;
  12.            case HOOKS:         /* Stall and then return */
  13.            case FINALIZERS:
  14.                break;
  15.            }
  16.        }
  17.        synchronized (Shutdown.class) {
  18.            // 執行序列
  19.            sequence();
  20.        }
  21.    }
  22.    // 而 sequence() 則會呼叫 runHooks(), 呼叫自定義的鉤子任務
  23.    private static void sequence() {
  24.        synchronized (lock) {
  25.            /* Guard against the possibility of a daemon thread invoking exit
  26.             * after DestroyJavaVM initiates the shutdown sequence
  27.             */
  28.            if (state != HOOKS) return;
  29.        }
  30.        runHooks();
  31.        boolean rfoe;
  32.        synchronized (lock) {
  33.            state = FINALIZERS;
  34.            rfoe = runFinalizersOnExit;
  35.        }
  36.        if (rfoe) runAllFinalizers();
  37.    }
  38.    // 執行鉤子,此處最多允許註冊 10 個鉤子,且進行同步呼叫,當然這是最頂級的鉤子,鉤子下還可以新增鉤子,可以任意新增n個
  39.    private static void runHooks() {
  40.        for (int i=0; i < MAX_SYSTEM_HOOKS; i++) {
  41.            try {
  42.                Runnable hook;
  43.                synchronized (lock) {
  44.                    // acquire the lock to make sure the hook registered during
  45.                    // shutdown is visible here.
  46.                    currentRunningHook = i;
  47.                    hook = hooks[i];
  48.                }
  49.                // 同步呼叫註冊的hook, 即 前面看到 ApplicationShutdownHooks.runHooks()
  50.                if (hook != null) hook.run();
  51.            } catch(Throwable t) {
  52.                if (t instanceof ThreadDeath) {
  53.                    ThreadDeath td = (ThreadDeath)t;
  54.                    throw td;
  55.                }
  56.            }
  57.        }
  58.    }

如此,整個關閉流程完美了。

簡化為:

  1. 註冊流程(應用主動呼叫):Runtime.addShutdownHook -> ApplicationShutdownHooks.add()/static -> java.lang.Shutdown.add()/shutdown()
  2. 執行流程(jvm自動呼叫):java.lang.Shutdown.shutdown()->sequence()->runHooks() -> ApplicationShutdownHooks.runHooks() -> hooks 最終

 

出處:https://www.cnblogs.com/yougewe/p/9881874.html

 

好 RESTful API 的設計原則

資料庫最佳化的幾個階段

怎麼理解Condition?

如何擴充套件和最佳化執行緒池?

多執行緒:為什麼在while迴圈中加入System.out.println,執行緒可以停止

快速排序演演算法到底有多快?

微服務實戰:使用API Gateway

MySQL的索引是什麼?怎麼最佳化?

在一個千萬級的資料庫查尋中,如何提高查詢效率?

 

END

>>>>>> 加群交流技術 <<<<<<

 

    贊(0)

    分享創造快樂