通过JDK源码分析关闭钩子详解
作者:汪洋之舟---seaboat
关闭钩子
用户关闭关闭程序,需要做一些善后的清理工作,但问题是,某些用户不会按照推荐的方法关闭应用程序,肯能导致善后工作无法进行。像tomcat调用server的start方法启动容器,然后会逐级调用start。当发出关闭命令是会启动关闭功能,但是关闭可能会有一些意外产生,导致应用程序没有进入到我们制定的关闭方法去。如何解决这个问题呢,使得即使有意外也能正常进入关闭流程。
好在java提供了一种优雅的方式去解决这种问题。使得关闭的善后处理的代码能执行。java的关闭钩子能确保总是执行,无论用户如何终止应用程序。除非用户kill,这个是个死穴。
Java提供了Shutdown Hook机制,它让我们在程序正常退出或者发生异常时能有机会做一些清场工作。使用的方法也很简单,Java.Runtime.addShutdownHook(Thread hook)
即可。关闭钩子其实可以看成是一个已经初始化了的但还没启动的线程,当JVM关闭时会并发地执行注册的所有关闭钩子。
对java而言,虚拟机会对以下几种操作进行关闭:
(1)系统调用System.exit()
方法
(2)程序最后一个守护线程退出时,应用程序正常退出。
(3)用户强行中断程序运行,比如ctrl+c等其他方式中断java程序
关闭钩子的生成:
1.创建Thread的子类
2.实现run方法,应用程序关闭时会调用该方法,不需要调用start方法
3.在应用中实例化关闭钩子类
4.使用Runtime注册关闭钩子
钩子执行时机
向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 方法让其执行。
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对脚本之家的支持。