开发者社区> sea-boat> 正文

从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

欢迎关注:

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

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
阿里云服务器端口号设置
阿里云服务器初级使用者可能面临的问题之一. 使用tomcat或者其他服务器软件设置端口号后,比如 一些不是默认的, mysql的 3306, mssql的1433,有时候打不开网页, 原因是没有在ecs安全组去设置这个端口号. 解决: 点击ecs下网络和安全下的安全组 在弹出的安全组中,如果没有就新建安全组,然后点击配置规则 最后如上图点击添加...或快速创建.   have fun!  将编程看作是一门艺术,而不单单是个技术。
20700 0
centos7 网速很慢解决办法----关闭ipv6
IPv6访问网站 centos7.1下,默认就启用了IPv6,所以可以通过网卡查看里面的。执行ifconfig命令  禁用IPv6 编辑文件 vi /etc/default/grub 修改红色部分修改后如下 (红色部分) GRUB_TIMEOUT=5 GRUB_DISTRIBUTOR="$(sed 's, release .*$,,g' /etc/system-release)" G
4466 0
阿里云服务器怎么设置密码?怎么停机?怎么重启服务器?
如果在创建实例时没有设置密码,或者密码丢失,您可以在控制台上重新设置实例的登录密码。本文仅描述如何在 ECS 管理控制台上修改实例登录密码。
23581 0
LinkedList 源码分析(JDK 1.8)
1.概述 LinkedList 是 Java 集合框架中一个重要的实现,其底层采用的双向链表结构。和 ArrayList 一样,LinkedList 也支持空值和重复值。由于 LinkedList 基于链表实现,存储元素过程中,无需像 ArrayList 那样进行扩容。
991 0
centos6.9 编译openJdk源码
仿照《深入理解Java虚拟机:JVM高级特性与最佳实践》编译openJdk源码 Linux系统 CentOS release 6.9 (Final) Kernel \r on an \m Bootstrap JDK java-1.
1000 0
阿里云服务器如何登录?阿里云服务器的三种登录方法
购买阿里云ECS云服务器后如何登录?场景不同,大概有三种登录方式:
13575 0
+关注
sea-boat
擅长篮球、跑步、游泳、羽毛球、编程、看书、写书的顾家好男人! Java深度、大数据、中间件、搜索引擎、机器学习、深度学习、Python、C++、开源! 《Tomcat内核设计剖析》作者。
276
文章
0
问答
文章排行榜
最热
最新
相关电子书
更多
JS零基础入门教程(上册)
立即下载
性能优化方法论
立即下载
手把手学习日志服务SLS,云启实验室实战指南
立即下载