從JDK原始碼看關閉鉤子

weixin_34377065發表於2017-10-23

關閉鉤子

Java提供了Shutdown Hook機制,它讓我們在程式正常退出或者發生異常時能有機會做一些清場工作。使用的方法也很簡單,Java.Runtime.addShutdownHook(Thread hook)即可。關閉鉤子其實可以看成是一個已經初始化了的但還沒啟動的執行緒,當JVM關閉時會併發地執行註冊的所有關閉鉤子。

鉤子執行時機

向JVM註冊關閉鉤子後的什麼時候會被呼叫,什麼時候不會被呼叫呢?分成以下情況:

  • Java程式正常執行完退出時會被呼叫。
  • windows和linux終端中通過ctrl-c終止命令時會被呼叫。
  • JVM發生OutOfMemory而退出時會被呼叫。
  • Java程式中執行System.exit()時會被呼叫。
  • 作業系統關閉時會被呼叫。
  • linux通過kill pid(除了kill -9 pid)結束程式時會被呼叫。
  • windows直接結束程式時不會被呼叫。

新增刪除鉤子

鉤子的新增和刪除都是通過 Runtime 來實現,裡面的實現也比較簡單,可以看到 addShutdownHook 和 removeShutdownHook 方法都是先通過安全管理器先檢查是否有 shutdownHooks 的許可權,然後再通過 ApplicationShutdownHooks 新增和刪除鉤子。

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);
    }複製程式碼

ApplicationShutdownHooks保管鉤子

ApplicationShutdownHooks 可以看成是用來保管所有關閉鉤子的容器,而主要是通過一個 IdentityHashMap 型別的變數來儲存鉤子。

private static IdentityHashMap<Thread, Thread> hooks;複製程式碼

有了 hooks 這個變數,新增刪除鉤子就是直接向這個 HashMap 進行 put 和 remove 操作了,其中在操作前也會做一些檢查,比如新增鉤子前會做三個判斷:

  1. 所有鉤子是否已經開始執行了,hooks 為 null 即表示所有關閉鉤子已經開始執行,此時不能再新增了。
  2. 鉤子狀態是否為 alive ,是則表示鉤子已經在執行,不能新增了。
  3. 是否已經包含了該鉤子,已包含則不能再新增。

類似的判斷邏輯還有 remove 操作。

    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;
    }複製程式碼

而 ApplicationShutdownHooks 中真正負責啟動所有鉤子的任務由 runHooks 方法負責,它的邏輯如下:

  1. 先對 ApplicationShutdownHooks 類加鎖並取到所有鉤子,然後將 hooks 變數設為 null 。
  2. 遍歷所有鉤子,分別啟動鉤子,前面有說到關閉鉤子其實可以看成是一個已經初始化了的但還沒啟動的執行緒,這裡呼叫 start 方法將其啟動即可。
  3. 用 join 方法協調所有鉤子執行緒,等待他們執行完畢。

     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) { }
         }
     }複製程式碼

ApplicationShutdownHooks 的 runHooks 方法又是由誰負責呼叫的呢?如下,它其實是變成一個 Runnable 物件新增到 Shutdown 類中了,Runnable 的 run 方法負責呼叫 runHooks 方法。接下去就要看 Shutdown 類什麼時候執行該 Runnable 物件了。

Shutdown.add(1 , false ,
                new Runnable() {
                    public void run() {
                        runHooks();
                    }
                }
            );複製程式碼

Shutdown中的鉤子

ApplicationShutdownHooks 的 Runnable 物件新增到 Shutdown 中的邏輯如下,


private static final int RUNNING = 0;
private static final int HOOKS = 1;
private static final int FINALIZERS = 2;

private static final int MAX_SYSTEM_HOOKS = 10;
private static final Runnable[] hooks = new Runnable[MAX_SYSTEM_HOOKS];

static void add(int slot, boolean registerShutdownInProgress, Runnable hook) {
        synchronized (lock) {
            if (hooks[slot] != null)
                throw new InternalError("Shutdown hook at slot " + slot + " already registered");

            if (!registerShutdownInProgress) {
                if (state > RUNNING)
                    throw new IllegalStateException("Shutdown in progress");
            } else {
                if (state > HOOKS || (state == HOOKS && slot <= currentRunningHook))
                    throw new IllegalStateException("Shutdown in progress");
            }

            hooks[slot] = hook;
        }
    }複製程式碼

slot表示將Runnable物件賦給 hooks 陣列中的哪個元素中, Shutdown 中同樣有一個 hooks 變數,它是 Runnable[] 型別,長度為 MAX_SYSTEM_HOOKS ,即為 10 。這個陣列可以看成是鉤子的優先順序實現,陣列下標用於表示優先順序,slot = 1 則表示賦值到陣列中第二個元素。

registerShutdownInProgress 表示是否允許註冊鉤子,即使正在執行 shutdown 。前面傳入 false ,顯然是不允許。其中 state > RUNNING 條件表示其他狀態都要丟擲異常,除非是 RUNNING 狀態,這個很好理解,一共有三個狀態,RUNNING、HOOKS、FINALIZERS,值分別為0、1、2。如果 registerShutdownInProgress 為 true 則只要不為 FINALIZERS 狀態,同時 slot 也要大於當前鉤子陣列的下標即可。

在前面說到的鉤子執行時機的情況下,JVM都會呼叫到 Shutdown 類的 sequence 方法,如下,

private static void sequence() {
        synchronized (lock) {
            if (state != HOOKS) return;
        }
        runHooks();
        boolean rfoe;
        synchronized (lock) {
            state = FINALIZERS;
            rfoe = runFinalizersOnExit;
        }
        if (rfoe) runAllFinalizers();
    }

private static void runHooks() {
        for (int i=0; i < MAX_SYSTEM_HOOKS; i++) {
            try {
                Runnable hook;
                synchronized (lock) {
                    currentRunningHook = i;
                    hook = hooks[i];
                }
                if (hook != null) hook.run();
            } catch(Throwable t) {
                if (t instanceof ThreadDeath) {
                    ThreadDeath td = (ThreadDeath)t;
                    throw td;
                }
            }
        }
    }複製程式碼

首先判斷當前狀態不等於 HOOKS 則直接返回,接著執行 runHooks 方法,這個方法也是我們主要要看的方法。然後再將狀態設為 FINALIZERS ,最後如果需要的話還要呼叫 runAllFinalizers 方法執行所有 finalizer。所以在JVM關閉時 runHooks 方法是會被呼叫的。

runHooks 方法邏輯簡單,就是遍歷 Runnable 陣列,一個個呼叫其 run 方法讓其執行。

以下是廣告相關閱讀

========廣告時間========

鄙人的新書《Tomcat核心設計剖析》已經在京東銷售了,有需要的朋友可以到 item.jd.com/12185360.ht… 進行預定。感謝各位朋友。

為什麼寫《Tomcat核心設計剖析》

=========================

相關閱讀:

從JDK原始碼角度看Object
從JDK原始碼角度看Long
從JDK原始碼角度看Integer
從JDK原始碼角度看Float
volatile足以保證資料同步嗎
談談Java基礎資料型別
從JDK原始碼角度看併發鎖的優化
從JDK原始碼角度看執行緒的阻塞和喚醒
從JDK原始碼角度看併發競爭的超時
從JDK原始碼角度看java併發執行緒的中斷
從JDK原始碼角度看Java併發的公平性
從JDK原始碼角度看java併發的原子性如何保證
從JDK原始碼角度看Byte
從JDK原始碼角度看Boolean
從JDK原始碼角度看Short
從JDK原始碼看System.exit

歡迎關注:

這裡寫圖片描述

這裡寫圖片描述

相關文章