乐趣区

关于java:Java-Shutdown-Hook-场景使用和源码分析

大家好,我是桃子,后盾私信【材料】,即可取得我精心整顿的技术材料,电子书籍,一线大厂面试材料和优良简历模板。

背景

如果想在 Java 过程退出时,包含失常和异样退出,做一些额定解决工作,例如资源清理,对象销毁,内存数据长久化到磁盘,期待线程池解决完所有工作等等。特地是过程异样挂掉的状况,如果一些重要状态没及时保留下来,或线程池的工作没被解决完,有可能会造成重大问题。那该怎么办呢?

Java 中的 Shutdown Hook 提供了比拟好的计划。咱们能够通过 Java.Runtime.addShutdownHook(Thread hook) 办法向 JVM 注册敞开钩子,在 JVM 退出之前会主动调用执行钩子办法,做一些结尾操作,从而让过程平滑优雅的退出,保障了业务的完整性。

Shutdown Hook 介绍

其实,shutdown hook 就是一个简略的 已初始化 然而 未启动 线程。当虚拟机开始敞开时,它将会调用所有已注册的钩子,这些钩子执行是并发的,执行程序是不确定的。

在虚拟机敞开的过程中,还能够持续注册新的钩子,或者撤销曾经注册过的钩子。不过有可能会抛出 IllegalStateException。注册和登记钩子的办法定义如下:

public void addShutdownHook(Thread hook) {// 省略}

public void removeShutdownHook(Thread hook) {// 省略}

敞开钩子被调用场景

敞开钩子能够在以下几种场景被调用:

  1. 程序失常退出
  2. 程序调用 System.exit() 退出
  3. 终端应用 Ctrl+C 中断程序
  4. 程序抛出异样导致程序退出,例如 OOM,数组越界等异样
  5. 零碎事件,例如用户登记或关闭系统
  6. 应用 Kill pid 命令杀掉过程,留神应用 kill -9 pid 强制杀掉不会触发执行钩子
    • *

验证程序失常退出状况

package com.chenpi;

public class ShutdownHookDemo {

    static {Runtime.getRuntime().addShutdownHook(new Thread(() -> System.out.println("执行钩子办法...")));
    }

    public static void main(String[] args) throws InterruptedException {System.out.println("程序开始启动...");
        Thread.sleep(2000);
        System.out.println("程序行将退出...");
    }
}

运行后果

程序开始启动...
程序行将退出...
执行钩子办法...

Process finished with exit code 0

验证程序调用 System.exit() 退出状况

package com.chenpi;

public class ShutdownHookDemo {

    static {Runtime.getRuntime().addShutdownHook(new Thread(() -> System.out.println("执行钩子办法...")));
    }

    public static void main(String[] args) throws InterruptedException {System.out.println("程序开始启动...");
        Thread.sleep(2000);
        System.exit(-1);
        System.out.println("程序行将退出...");
    }
}

运行后果

程序开始启动...
执行钩子办法...

Process finished with exit code -1

验证终端应用 Ctrl+C 中断程序,在命令行窗口中运行程序,而后应用 Ctrl+C 中断

package com.chenpi;

public class ShutdownHookDemo {

    static {Runtime.getRuntime().addShutdownHook(new Thread(() -> System.out.println("执行钩子办法...")));
    }

    public static void main(String[] args) throws InterruptedException {System.out.println("程序开始启动...");
        Thread.sleep(2000);
        System.out.println("程序行将退出...");
    }
}

运行后果

D:\IdeaProjects\java-demo\java ShutdownHookDemo
程序开始启动...
执行钩子办法...

演示抛出异样导致程序异样退出

package com.chenpi;

public class ShutdownHookDemo {

    static {Runtime.getRuntime().addShutdownHook(new Thread(() -> System.out.println("执行钩子办法...")));
    }

    public static void main(String[] args) {System.out.println("程序开始启动...");
        int a = 0;
        System.out.println(10 / a);
        System.out.println("程序行将退出...");
    }
}

运行后果

程序开始启动...
执行钩子办法...
Exception in thread "main" java.lang.ArithmeticException: / by zero
    at com.chenpi.ShutdownHookDemo.main(ShutdownHookDemo.java:12)

Process finished with exit code 1

至于零碎被敞开,或者应用 Kill pid 命令杀掉过程就不演示了,感兴趣的能够自行验证。

注意事项

能够向虚拟机注册多个敞开钩子,然而留神这些钩子执行是并发的,执行程序是不确定的。

package com.chenpi;

public class ShutdownHookDemo {

    static {Runtime.getRuntime().addShutdownHook(new Thread(() -> System.out.println("执行钩子办法 A...")));
        Runtime.getRuntime().addShutdownHook(new Thread(() -> System.out.println("执行钩子办法 B...")));
        Runtime.getRuntime().addShutdownHook(new Thread(() -> System.out.println("执行钩子办法 C...")));
    }

    public static void main(String[] args) throws InterruptedException {System.out.println("程序开始启动...");
        Thread.sleep(2000);
        System.out.println("程序行将退出...");
    }
}

运行后果

程序开始启动...
程序行将退出...
执行钩子办法 B...
执行钩子办法 C...
执行钩子办法 A...

向虚拟机注册的钩子办法须要尽快执行完结,尽量不要执行长时间的操作,例如 I/O 等可能被阻塞的操作,死锁等,这样就会导致程序短时间不能被敞开,甚至始终敞开不了。咱们也能够引入超时机制强制退出钩子,让程序失常完结。

package com.chenpi;

public class ShutdownHookDemo {

    static {Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            // 模仿长时间的操作
            try {Thread.sleep(1000000);
            } catch (InterruptedException e) {e.printStackTrace();
            }
        }));
    }

    public static void main(String[] args) throws InterruptedException {System.out.println("程序开始启动...");
        Thread.sleep(2000);
        System.out.println("程序行将退出...");
    }
}

以上的钩子执行工夫比拟长,最终会导致程序在期待很长时间之后能力被敞开。


如果 JVM 曾经调用执行敞开钩子的过程中,不容许注册新的钩子和登记曾经注册的钩子,否则会报 IllegalStateException 异样。通过源码剖析,JVM 调用钩子的时候,即调用 ApplicationShutdownHooks#runHooks() 办法,会将所有钩子从变量 hooks 取出,而后将此变量置为 null

// 调用执行钩子
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) {}}
}

在注册和登记钩子的办法中,首先会判断 hooks 变量是否为 null,如果为 null 则抛出异样。

// 注册钩子
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;
}

咱们演示下这种状况

package com.chenpi;

public class ShutdownHookDemo {

    static {Runtime.getRuntime().addShutdownHook(new Thread(() -> {System.out.println("执行钩子办法...");
            Runtime.getRuntime().addShutdownHook(new Thread(() -> System.out.println("在 JVM 调用钩子的过程中再新注册钩子,会报错 IllegalStateException")));
            // 在 JVM 调用钩子的过程中登记钩子,会报错 IllegalStateException
            Runtime.getRuntime().removeShutdownHook(Thread.currentThread());
        }));
    }

    public static void main(String[] args) throws InterruptedException {System.out.println("程序开始启动...");
        Thread.sleep(2000);
        System.out.println("程序行将退出...");
    }
}

运行后果

程序开始启动...
程序行将退出...
执行钩子办法...
Exception in thread "Thread-0" java.lang.IllegalStateException: Shutdown in progress
    at java.lang.ApplicationShutdownHooks.add(ApplicationShutdownHooks.java:66)
    at java.lang.Runtime.addShutdownHook(Runtime.java:211)
    at com.chenpi.ShutdownHookDemo.lambda$static$1(ShutdownHookDemo.java:8)
    at java.lang.Thread.run(Thread.java:748)

如果调用 Runtime.getRuntime().halt() 办法进行 JVM,那么虚拟机是不会调用钩子的。

package com.chenpi;

public class ShutdownHookDemo {

    static {Runtime.getRuntime().addShutdownHook(new Thread(() -> System.out.println("执行钩子办法...")));
    }

    public static void main(String[] args) {System.out.println("程序开始启动...");
        System.out.println("程序行将退出...");
        Runtime.getRuntime().halt(0);
    }
}

运行后果

程序开始启动...
程序行将退出...

Process finished with exit code 0

如果要想终止执行中的钩子办法,只能通过调用 Runtime.getRuntime().halt() 办法,强制让程序退出。在 Linux 环境中应用 kill -9 pid 命令也是能够强制终止退出。

package com.chenpi;

public class ShutdownHookDemo {

    static {Runtime.getRuntime().addShutdownHook(new Thread(() -> {System.out.println("开始执行钩子办法...");
            Runtime.getRuntime().halt(-1);
            System.out.println("完结执行钩子办法...");
        }));
    }

    public static void main(String[] args) {System.out.println("程序开始启动...");
        System.out.println("程序行将退出...");
    }
}

运行后果

程序开始启动...
程序行将退出...
开始执行钩子办法...

Process finished with exit code -1

如果程序应用 Java Security Managers,应用 shutdown Hook 则须要平安权限 RuntimePermission(“shutdownHooks”),否则会导致 SecurityException

实际

例如,咱们程序自定义了一个线程池,用来接管和解决工作。如果程序忽然奔溃异样退出,这时线程池的所有工作有可能还未解决实现,如果不解决完程序就间接退出,可能会导致数据失落,业务异样等重要问题。这时钩子就派上用场了。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class ShutdownHookDemo {
    // 线程池
    private static ExecutorService executorService = Executors.newFixedThreadPool(3);

    static {Runtime.getRuntime().addShutdownHook(new Thread(() -> {System.out.println("开始执行钩子办法...");
            // 敞开线程池
            executorService.shutdown();
            try {
                // 期待 60 秒
                System.out.println(executorService.awaitTermination(60, TimeUnit.SECONDS));
            } catch (InterruptedException e) {e.printStackTrace();
            }
            System.out.println("完结执行钩子办法...");
        }));
    }

    public static void main(String[] args) throws InterruptedException {System.out.println("程序开始启动...");
        // 向线程池增加 10 个工作
        for (int i = 0; i < 10; i++) {Thread.sleep(1000);
            final int finalI = i;
            executorService.execute(() -> {
                try {Thread.sleep(4000);
                } catch (InterruptedException e) {e.printStackTrace();
                }
                System.out.println("Task" + finalI + "execute...");
            });
            System.out.println("Task" + finalI + "is in thread pool...");
        }
    }
}

在命令行窗口中运行程序,在 10 个工作都提交到线程池之后,工作都还未解决实现之前,应用 Ctrl+C 中断程序,最终在虚拟机敞开之前,调用了敞开钩子,敞开线程池,并且期待 60 秒让所有工作执行实现。

Shutdown Hook 在 Spring 中的使用

Shutdown Hook 在 Spring 中是如何使用的呢。通过源码剖析,Springboot 我的项目启动时会判断 registerShutdownHook 的值是否为 true,默认是 true,如果为真则向虚拟机注册敞开钩子。

private void refreshContext(ConfigurableApplicationContext context) {refresh(context);
    if (this.registerShutdownHook) {
        try {context.registerShutdownHook();
        }
        catch (AccessControlException ex) {// Not allowed in some environments.}
    }
}

@Override
public void registerShutdownHook() {if (this.shutdownHook == null) {
        // No shutdown hook registered yet.
        this.shutdownHook = new Thread() {
            @Override
            public void run() {synchronized (startupShutdownMonitor) {
                    // 钩子办法
                    doClose();}
            }
        };
        // 底层还是应用此办法注册钩子
        Runtime.getRuntime().addShutdownHook(this.shutdownHook);
    }
}

在敞开钩子的办法 doClose 中,会做一些虚拟机敞开前解决工作,例如销毁容器里所有单例 Bean,敞开 BeanFactory,公布敞开事件等等。

protected void doClose() {
    // Check whether an actual close attempt is necessary...
    if (this.active.get() && this.closed.compareAndSet(false, true)) {if (logger.isDebugEnabled()) {logger.debug("Closing" + this);
        }

        LiveBeansView.unregisterApplicationContext(this);

        try {
            // 公布 Spring 利用上下文的敞开事件,让监听器在利用敞开之前做出响应解决
            publishEvent(new ContextClosedEvent(this));
        }
        catch (Throwable ex) {logger.warn("Exception thrown from ApplicationListener handling ContextClosedEvent", ex);
        }

        // Stop all Lifecycle beans, to avoid delays during individual destruction.
        if (this.lifecycleProcessor != null) {
            try {
                // 执行 lifecycleProcessor 的敞开办法
                this.lifecycleProcessor.onClose();}
            catch (Throwable ex) {logger.warn("Exception thrown from LifecycleProcessor on context close", ex);
            }
        }

        // 销毁容器里所有单例 Bean
        destroyBeans();

        // 敞开 BeanFactory
        closeBeanFactory();

        // Let subclasses do some final clean-up if they wish...
        onClose();

        // Reset local application listeners to pre-refresh state.
        if (this.earlyApplicationListeners != null) {this.applicationListeners.clear();
            this.applicationListeners.addAll(this.earlyApplicationListeners);
        }

        // Switch to inactive.
        this.active.set(false);
    }
}

咱们晓得,咱们能够定义 bean 并且实现 DisposableBean 接口,重写 destroy 对象销毁办法。destroy 办法就是在 Spring 注册的敞开钩子里被调用的。例如咱们应用 Spring 框架的 ThreadPoolTaskExecutor 线程池类,它就实现了 DisposableBean 接口,重写了 destroy 办法,从而在程序退出前,进行线程池销毁工作。源码如下:

@Override
public void destroy() {shutdown();
}

/**
 * Perform a shutdown on the underlying ExecutorService.
 * @see java.util.concurrent.ExecutorService#shutdown()
 * @see java.util.concurrent.ExecutorService#shutdownNow()
 */
public void shutdown() {if (logger.isInfoEnabled()) {logger.info("Shutting down ExecutorService" + (this.beanName != null ? "'" + this.beanName + "'":""));
    }
    if (this.executor != null) {if (this.waitForTasksToCompleteOnShutdown) {this.executor.shutdown();
        }
        else {for (Runnable remainingTask : this.executor.shutdownNow()) {cancelRemainingTask(remainingTask);
            }
        }
        awaitTerminationIfNecessary(this.executor);
    }
}
退出移动版