参考资料:
写在开头:本文为学习后的总结,可能有不到位的地方,错误的地方,欢迎各位指正。
目录
在Java程序退出时,我们可能需要先执行一些善后工作,如关闭线程池、连接池、文件句柄等,即所谓“优雅停机”(graceful shutdown)。如何保证善后工作的代码能够被执行到呢?Java为用户提供了关闭钩子(shutdown hook)。
这些钩子可以在应用关闭时帮助我们完成JVM退出前的善后工作,例如删除临时文件,或者清除无法由操作系统自动清除的资源。
关闭钩子在以下情景都会被调用:
关闭钩子在以下情景不会被调用:
我们可以通过Runtime.getRuntime().addShutdownHook()方法来注册关闭钩子:
- Runtime.getRuntime().addShutdownHook(new Thread(() -> {
- System.out.println("auto clean temporary file");
- }));
注意:这里的钩子方法必须是Thread的子类,换句话说,钩子方法是异步执行的。
我们通过下面的例子演示下多个钩子方法的执行:
- public class T {
- @SuppressWarnings("deprecation")
- public static void main(String[] args) throws Exception {
-
- MyHook hook1 = new MyHook("Hook1");
- MyHook hook2 = new MyHook("Hook2");
- MyHook hook3 = new MyHook("Hook3");
-
- //注册关闭钩子
- Runtime.getRuntime().addShutdownHook(hook1);
- Runtime.getRuntime().addShutdownHook(hook2);
- Runtime.getRuntime().addShutdownHook(hook3);
-
- //移除关闭钩子
- Runtime.getRuntime().removeShutdownHook(hook3);
-
- //Main线程将在执行这句之后退出
- System.out.println("Main Thread Ends.");
- }
- }
-
- class MyHook extends Thread {
- private String name;
- public MyHook (String name) {
- this.name = name;
- setName(name);
- }
- public void run() {
- System.out.println(name + " Ends.");
- }
- }
可以看到,main函数执行完成,首先输出的是Main Thread Ends,接下来执行关闭钩子,输出Hook2 Ends和Hook1 Ends。这两行也可以证实:关闭钩子的本质就是已经初始化但在JVM关闭之前最后一刻才会执行的线程,并且JVM不是以注册的顺序来调用关闭钩子的。而由于hook3在调用了addShutdownHook后,接着对其调用了removeShutdownHook将其移除,于是hook3在JVM退出时没有执行,因此没有输出Hook3 Ends。
- Main Thread Ends.
- Hook2 Ends.
- Hook1 Ends.
我们先从注册钩子的源码开始分析,于是我们找到Runtime类中的addShutdownHook()方法。
- public void addShutdownHook(Thread hook) {
- SecurityManager sm = System.getSecurityManager();
- if (sm != null) {
- sm.checkPermission(new RuntimePermission("shutdownHooks"));
- }
- ApplicationShutdownHooks.add(hook);
- }
addShutdownHook()方法实际上是代理了ApplicationShutdownHooks.add()方法。
ApplicationShutdownHooks类会初始化一个map容器来存储注册进来的钩子。在注册关闭钩子之前,会先判断是否符合以下三个条件,如果是,则钩子无法注册:
- class ApplicationShutdownHooks {
-
- // 用来存放钩子的容器
- private static IdentityHashMap
hooks; -
- private ApplicationShutdownHooks() {}
-
- // 注册方法
- static synchronized void add(Thread hook) {
- // JVM正在关闭,即钩子已经被触发(此时IdentityHashMap为null)
- 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(),当被调用时,该方法会挂起调用线程,等待被调用线程(在这里就是钩子线程)执行完毕之后,调用线程才继续执行。也就是说,这里必须保证关闭钩子在主线程真正关闭之前执行完毕。
- static void runHooks() {
- Collection
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) {
- }
- }
- }
- }
runHooks()由ApplicationShutdownHooks方法的static代码块中注册为了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)。
registerShutdownInProgress表示是否允许在关闭过程中注册钩子,前面传入的是false。如果为true的话,则可以在当前运行的钩子后面注册优先级更低的钩子。
- 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;
- }
- }
JVM关闭时将逐步调用到shutdown类的exit方法。
- // System.java
- public static void exit(int status) {
- Runtime.getRuntime().exit(status);
- }
-
- // Runtime.java
- public void exit(int status) {
- SecurityManager security = System.getSecurityManager();
- if (security != null) {
- security.checkExit(status);
- }
- Shutdown.exit(status);
- }
此时会执行到sequence()方法,钩子方法的调用正式改方法内执行的。
- static void exit(int status) {
- // 其余代码
- synchronized (Shutdown.class) {
- sequence();
- halt(status);
- }
- }
-
- // 调用钩子方法
- private static void sequence() {
- synchronized (lock) {
- if (state != HOOKS) return;
- }
- runHooks();
- boolean rfoe;
- synchronized (lock) {
- state = FINALIZERS;
- rfoe = runFinalizersOnExit;
- }
- if (rfoe) runAllFinalizers();
- }
-
- // 真正关闭JVM的方法
- static void halt(int status) {
- synchronized (haltLock) {
- halt0(status);
- }
- }
有一些第三方组件在代码中注册了关闭自身资源的ShutdownHook,这些ShutdownHook对于我们的平滑退出有时候起了反作用。
从Runtime.java和ApplicationShutdownHooks.java的源码中看到ApplicationShutdownHooks不是public的,类中的hooks也是private的。只有通过反射的方式才能获取并控制它们。定义ExcludeIdentityHashMap类来帮助我们阻止非自己的ShutdownHook注入。
- class ExcludeIdentityHashMap
extends IdentityHashMap { -
- public V put(K key, V value) {
- if (key instanceof Thread) {
- Thread thread = (Thread) key;
- if (!thread.getName().startsWith("My-")) {
- return value;
- }
- }
- return super.put(key, value);
- }
- }
通过反射的方式注入自己的ShutdownHook并清除其他Thread
- String className = "java.lang.ApplicationShutdownHooks";
- Class> clazz = Class.forName(className);
- Field field = clazz.getDeclaredField("hooks");
- field.setAccessible(true);
-
- Thread shutdownThread = new Thread(new Runnable() {
- @Override
- public void run() {
- // TODO
- }
- });
- shutdownThread.setName("My-WebShutdownThread");
- IdentityHashMap
excludeIdentityHashMap = new ExcludeIdentityHashMap<>(); - excludeIdentityHashMap.put(shutdownThread, shutdownThread);
-
- synchronized (clazz) {
- IdentityHashMap
map = (IdentityHashMap) field.get(clazz); - for (Thread thread : map.keySet()) {
- Log.info("found shutdownHook: " + thread.getName());
- excludeIdentityHashMap.put(thread, thread);
- }
-
- field.set(clazz, excludeIdentityHashMap);
- }
如果在关闭钩子中关闭应用程序的公共的组件,如日志服务,或者数据库连接等,像下面这样:
- Runtime.getRuntime().addShutdownHook(new Thread() {
- public void run() {
- try {
- LogService.this.stop();
- } catch (InterruptedException ignored){
- //ignored
- }
- }
- });
由于关闭钩子将并发执行,因此在关闭日志时可能导致其他需要日志服务的关闭钩子产生问题。为了避免这种情况,可以使关闭钩子不依赖那些可能被应用程序或其他关闭钩子关闭的服务。
实现这种功能的一种方式是对所有服务使用同一个关闭钩子(而不是每个服务使用一个不同的关闭钩子),并且在该关闭钩子中执行一系列的关闭操作。这确保了关闭操作在单个线程中串行执行,从而避免了在关闭操作之前出现竞态条件或死锁等问题。