从JDK源码看关闭钩子

简介: 关闭钩子 Java提供了Shutdown Hook机制,它让我们在程序正常退出或者发生异常时能有机会做一些清场工作。
+关注继续查看

关闭钩子

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内核设计剖析》已经在京东销售了,有需要的朋友可以到 https://item.jd.com/12185360.html 进行预定。感谢各位朋友。

为什么写《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

欢迎关注:

这里写图片描述
这里写图片描述

目录
相关文章
|
1月前
|
Java 调度
《JavaSE-第二十一章》之线程的状态与中断
《JavaSE-第二十一章》之线程的状态与中断
|
8月前
|
数据可视化 Java
Java中的事件监听知识点&动作监听简单实现(含源码阅读)
监听分为三类:键盘监听、鼠标监听 和 动作监听,看到这些名词可能有点懵,监听是个啥,那么首先我们来理解一下绑定监听:当事件源(按钮)上发生了某个事件(单击),就执行某段代码(输出“您已点击按钮”)
177 0
Java中的事件监听知识点&动作监听简单实现(含源码阅读)
|
10月前
|
Java API
Java虚拟机关闭钩子(Shutdown hook)
Shutdown hook是Jvm关闭的钩子,是通过Runtime#addShutdownHook(Thread hook)方法来实现的,根据api是注解可知它就是一系例的已初始化但尚未执行的线程对象。我们可以通过向Jvm注册一个钩子,实现在程序退出时关闭资源、平滑退出的功能。所谓的优雅停机也可以这么搞。
252 0
|
安全 Java 应用服务中间件
通过JDK源码分析关闭钩子详解
通过JDK源码分析关闭钩子详解
通过JDK源码分析关闭钩子详解
|
分布式计算 Java Hadoop
如何优雅地关闭JVM?看看钩子函数如何一步实现
程序的启动很简单,启动的时候通常会做一些预加载资源的操作。但是有时候关闭的时候,启动的时候预加载的资源并没有完全清理干净,因此可以使用钩子函数来完成。
96 0
如何优雅地关闭JVM?看看钩子函数如何一步实现
|
Java API 调度
一文解说Java的线程生命周期状态以及常用方法调用之后的线程状态
一般操作系统的线程状态都有哪些?Java中的线程周期状态的生命周期状态都有哪些?Java中线程状态是如何转化的?Java中线程常用方法有哪些?
113 0
|
Java Maven
java 异步线程监听、结果回调、异常捕获 | Java工具类
java 异步线程监听、结果回调、异常捕获 | Java工具类
java 异步线程监听、结果回调、异常捕获 | Java工具类
【EventBus】EventBus 源码解析 ( 事件发送 | 发布线程为 子线程 切换到 主线程 执行订阅方法的过程分析 )
【EventBus】EventBus 源码解析 ( 事件发送 | 发布线程为 子线程 切换到 主线程 执行订阅方法的过程分析 )
115 0
浅析java的退出钩子(Hook)
浅析java的退出钩子(Hook)
355 0
浅析java的退出钩子(Hook)