美文网首页JVM
聊聊Java的关闭钩子(shutdown hook)

聊聊Java的关闭钩子(shutdown hook)

作者: LittleMagic | 来源:发表于2020-06-06 21:16 被阅读0次

    这几天忙到飞起,加上简书被请去喝了两天茶,还是写篇炒鸡容易的吧。

    在Java程序退出时——尤其是非正常退出时,我们可能需要先执行一些善后工作,如关闭线程池、连接池、文件句柄等,即所谓“优雅停机”(graceful shutdown)。如何保证善后工作的代码能够被执行到呢?Java为用户提供了关闭钩子(shutdown hook),它在以下情景都会被调用:

    • 程序正常退出,即最后一个非守护线程结束时;
    • 程序中执行到了System.exit()方法;
    • 终端接收到了CTRL-C中断,或者注销登录;
    • 通过kill命令杀死进程(但是kill -9除外)。

    关闭钩子在以下情景不会被调用:

    • 通过kill -9命令杀死进程——所以kill -9一定要慎用;
    • 程序中执行到了Runtime.getRuntime().halt()方法;
    • 操作系统突然崩溃,或机器掉电。

    用户通过Runtime.getRuntime().addShutdownHook()方法来注册关闭钩子,示例:

    Runtime.getRuntime().addShutdownHook(new Thread(() -> {
      someThreadPool.shutdown();
      someConnectionPool.close();
      LOGGER.info("Shutdown hook called");
    }));
    

    可见,关闭钩子的本质就是已经初始化但在JVM关闭之前不会执行的线程。当然,Java也提供了removeShutdownHook()方法来删除关闭钩子。

    JDK内是通过ApplicationShutdownHooks类来维护关闭钩子的,该类中有一个IdentityHashMap容器。

    private static IdentityHashMap<Thread, Thread> hooks;
    

    addShutdownHook()方法实际上是代理了ApplicationShutdownHooks.add()方法。在注册关闭钩子之前,会先判断是否符合以下三个条件,如果是,则钩子无法注册:

    • JVM正在关闭,即钩子已经被触发(此时IdentityHashMap为null);
    • 当前关闭钩子正在执行;
    • IdentityHashMap中已经存在了要注册的钩子。
    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);
    }
    

    启动钩子的方法则是ApplicationShutdownHooks.runHooks()。

    static void runHooks() {
        Collection<Thread> threads;
        synchronized(ApplicationShutdownHooks.class) {
            threads = hooks.keySet();
            hooks = null;
        }
        for (Thread hook : threads) {
            hook.start();
        }
        for (Thread hook : threads) {
            while (true) {
                try {
                    hook.join();
                    break;
                } catch (InterruptedException ignored) {
                }
            }
        }
    }
    

    这里要注意Thread.join()方法的语义,即挂起调用线程,等待被调用线程(在这里就是钩子线程)执行完毕之后,调用线程才继续执行。也就是说,这里必须保证关闭钩子在主线程真正关闭之前执行完毕。

    那么runHooks()方法又是在哪里被调用的呢?由ApplicationShutdownHooks方法的static代码块可知是在Shutdown类,但是仍然包装成了Runnable,没有立即执行。

    static {
        try {
            Shutdown.add(1 /* shutdown hook invocation order */,
                false /* not registered if shutdown in progress */,
                new Runnable() {
                    public void run() {
                        runHooks();
                    }
                }
            );
            hooks = new IdentityHashMap<>();
        } catch (IllegalStateException e) {
            // application shutdown hooks cannot be added if
            // shutdown is in progress.
            hooks = null;
        }
    }
    

    Shutdown类内用Runnable的数组hooks维护关闭钩子的执行,并且该数组同时表示关闭钩子的优先级,排在前面slot的会先执行。虽然该数组的长度为10,但是目前只用了3个slot,用户注册的应用关闭钩子的优先级夹在两种系统钩子的中间(即固定占用slot 1)。

    private static final int RUNNING = 0;
    private static final int HOOKS = 1;
    private static final int FINALIZERS = 2;
    private static int state = RUNNING;
    
    // The system shutdown hooks are registered with a predefined slot.
    // The list of shutdown hooks is as follows:
    // (0) Console restore hook
    // (1) Application hooks
    // (2) DeleteOnExit hook
    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;
        }
    }
    

    registerShutdownInProgress表示是否允许在关闭过程中注册钩子,前面传入的是false。如果为true的话,则可以在当前运行的钩子后面注册优先级更低的钩子。

    加入到hooks数组的钩子最终会在sequence()方法触发执行,最后还会根据runFinalizersOnExit标志位来判断是否需要执行finalizer。runAllFinalizers()是一个native方法。

    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();
    }
    
    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];
                }
                if (hook != null) hook.run();
            } catch(Throwable t) {
                if (t instanceof ThreadDeath) {
                    ThreadDeath td = (ThreadDeath)t;
                    throw td;
                }
            }
        }
    }
    

    周末快乐,民那晚安(不是

    相关文章

      网友评论

        本文标题:聊聊Java的关闭钩子(shutdown hook)

        本文链接:https://www.haomeiwen.com/subject/ypkbohtx.html