从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

欢迎关注:

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

目录
相关文章
|
4月前
|
缓存 Dubbo Java
趁同事上厕所的时间,看完了 Dubbo SPI 的源码,瞬间觉得 JDK SPI 不香了
趁同事上厕所的时间,看完了 Dubbo SPI 的源码,瞬间觉得 JDK SPI 不香了
|
7月前
|
Java 容器
阿里内部流传的JDK源码剖析手册!GitHub已获上千万的访问量
相信现在已经有很多小伙伴知道了“微软”要对JDK下手了! JDK是什么? jdk是Java语言的软件开发工具包,主要用于移动设备、嵌入式设备上的java应用程序。jdk是整个java开发的核心,它包含了JAVA的运行环境和JAVA工具。相对而言,没有jdk的话,无法编译Java程序(指java源码.java文件),如果想只运行Java程序(指class或jar或其它归档文件),要确保已安装相应的JRE。
196 0
|
3月前
|
缓存 Java Spring
Spring 源码阅读 66:基于 JDK 的 AOP 代理如何获取拦截器链(4)- 将 Advice 封装为拦截器
【1月更文挑战第1天】本文分析了 Advice 被封装成 MethodInterceptor 的过程,Spring AOP 用到的五种 Advice 中,有些本身就是 MethodInterceptor 的实现类,而有些需要通过适配器的封装。
43 0
|
7月前
|
设计模式 Java 程序员
太爆了!阿里最新出品2023版JDK源码学习指南,Github三天已万赞
最近后台收到很多粉丝私信,说的是程序员究竟要不要去读源码?当下行情,面试什么样的薪资/岗位才会被问到源码? 对此,我的回答是:一定要去读,并且要提到日程上来! 据不完全统计,现在市面上不管是初级,中级,还是高级岗,面试的时候都有可能会问到源码中的问题,它已经成为程序员常规必备的一个技术点。如果你当下想通过一个面试,或者想把中级薪资要到相对于比较高的话,源码这块就必须要会。
94 0
|
1月前
|
算法 Java 索引
【数据结构与算法】4、双向链表(学习 jdk 的 LinkedList 部分源码)
【数据结构与算法】4、双向链表(学习 jdk 的 LinkedList 部分源码)
31 0
|
2月前
|
设计模式 Java
根据JDK源码Calendar来看工厂模式和建造者模式
根据JDK源码Calendar来看工厂模式和建造者模式
33 3
|
3月前
|
Java Linux iOS开发
Spring5源码(27)-静态代理模式和JDK、CGLIB动态代理
Spring5源码(27)-静态代理模式和JDK、CGLIB动态代理
23 0
|
3月前
|
XML Java 数据格式
Spring 源码阅读 70:基于 JDK 的 AOP 代理拦截器链执行(4)- 容易被忽略的 ExposeInvocationInterceptor
【1月更文挑战第5天】本文分析了 Spring AOP 拦截器链中的一个特殊拦截器 ExposeInvocationInterceptor 的注册的时机以及它的作用。至此,基于 JDK 的 AOP 代理拦截器链执行的逻辑就分析完了。
387 0
|
3月前
|
Java 索引 Spring
Spring 源码阅读 69:基于 JDK 的 AOP 代理拦截器链执行(3)- MethodInterceptor 分析
【1月更文挑战第4天】本文详细分析了 Spring AOP 中五种增强类型对应的拦截器中增强方法的执行逻辑,结合上一篇中分析的 ReflectiveMethodInvocation 中proceed方法的执行逻辑,就组成了完整的拦截器链递归调用的逻辑。
34 0