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


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

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

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

  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() {
                @Override
                public 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() {
            @Override
            public 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() {
            @Override
            public 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 了,你可以做很多:
  1. 關閉 socket 鏈接
  2. 清理臨時文件
  3. 發送消息通知給訂閱方,告知自己下線
  4. 將自己將要被銷毀的消息通知給子進程
  5. 各種資源的釋放
  ...

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

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 <tt>{@link Thread}</tt> 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
     *          <tt>{@link RuntimePermission}("shutdownHooks")</tt>
     *
     * @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;
                }
            }
        }
    }
    

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

簡化為: 

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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM