前言
如果想在 Java 进程退出时,包括正常和异常退出,做一些额外处理工作,例如资源清理,对象销毁,内存数据持久化到磁盘,等待线程池处理完所有任务等等。特别是进程异常挂掉的情况,如果一些重要状态没及时保留下来,或线程池的任务没被处理完,有可能会造成严重问题。那该怎么办呢?
Java 中的 Shutdown Hook 提供了比较好的方案。我们可以通过 Runtime#addShutdownHook
方法向 JVM 注册关闭钩子,在 JVM 退出之前会自动调用执行钩子方法,做一些结尾操作,从而让进程平滑优雅的退出,保证了业务的完整性。
版本约定
- JDK 版本:1.8.0_231
- Java SE API Documentation:https://docs.oracle.com/javase/8/docs/api/
正文
关闭钩子介绍
其实,Shutdown Hook 就是一个简单的已初始化但是未启动的线程。当虚拟机开始关闭时,它将会调用所有已注册的钩子,这些钩子执行是并发的,执行顺序是不确定的。
在虚拟机关闭的过程中,还可以继续注册新的钩子,或者撤销已经注册过的钩子。不过有可能会抛出 IllegalStateException。
在 Runtime 类中注册和注销钩子的方法定义如下:
Java SE API Documentation:https://docs.oracle.com/javase/8/docs/api/java/lang/Runtime.html
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);
}
先来看一个简单的例子:
public class ShutdownHook {
public static void main(String[] args) {
Runtime.getRuntime()
.addShutdownHook(new Thread(() -> System.out.println("Shutdown Hook is running !")));
System.out.println("Application Terminating ...");
}
}
运行程序,输出:
Application Terminating ...
Shutdown Hook is running !
可以看到 Shutdown Hook is running !
输出在 Application Terminating ...
之后
关闭钩子被调用场景
JVM 存在如下几种关闭的方式:
程序只有在正常关闭和异常关闭的情况下才会执行钩子方法做一些结尾操作,如果是强制关闭的则不会调用,强制关闭直接无商量的终止 JVM 进程,不给 JVM 喘息的机会。
验证程序正常退出情况。
public class ShutdownHook {
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("程序即将退出...");
}
}
运行程序,输出:
程序开始启动...
程序即将退出...
执行钩子方法...
验证程序调用 System.exit() 退出情况。
public class ShutdownHook {
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("程序即将退出...");
}
}
运行程序,输出:
程序开始启动...
执行钩子方法...
验证终端使用 Ctrl+C 中断程序,在命令行窗口中运行程序,然后使用 Ctrl+C 中断。
public class ShutdownHook {
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
程序开始启动...
执行钩子方法...
演示抛出异常导致程序异常退出。
public class ShutdownHook {
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 test12.ShutdownHook.main(ShutdownHook.java:12)
至于系统被关闭,或者使用 Kill pid 命令杀掉进程就不演示了,感兴趣的可以自行验证。
使用关闭钩子的注意事项
如果 JVM 由于某些内部错误而崩溃,则它可能崩溃而没有机会执行一条指令。另外,如果操作系统发出 SIGKILL 信号(在 Unix / Linux 中为 kill -9)或 TerminateProcess(Windows),则要求应用程序立即终止而无需甚至在等待任何清理活动。除上述内容外,还可以通过调用 Runtime.halt() 方法来终止 JVM,而不允许运行 Shutdown Hook。
Shutdown Hook 本质上是一个线程(也称为 Hook 线程),对于一个 JVM中 注册的多个关闭钩子,它们将会并发执行,所以 JVM 并不保证它们的执行顺序,由于是并发执行的,那么很可能因为代码不当导致出现竞态条件或死锁等问题。
public class ShutdownHook {
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("程序即将退出...");
}
}
运行程序,输出:
程序开始启动...
程序即将退出...
执行钩子方法A...
执行钩子方法C...
执行钩子方法B...
Hook 线程会延迟 JVM 的关闭时间,这就要求在编写钩子过程中必须要尽可能的减少 Hook 线程的执行时间,避免 Hook 线程中出现耗时的计算、等待用户 I/O 等等操作。
如果 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 则抛出异常。 ```java // 注册钩子 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; }
我们演示下这种情况:
```java
public class ShutdownHook {
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 test12.ShutdownHook.lambda$static$1(ShutdownHook.java:8)
at java.lang.Thread.run(Thread.java:748)
如果调用 Runtime.getRuntime().halt() 方法停止 JVM,那么虚拟机是不会调用钩子的。
public class ShutdownHook {
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);
}
}
运行程序,输出:
程序开始启动...
程序即将退出...
如果要想终止执行中的钩子方法,只能通过调用 Runtime.getRuntime().halt() 方法,强制让程序退出。在 Linux 环境中使用 kill -9 pid 命令也是可以强制终止退出。
public class ShutdownHook {
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("程序即将退出...");
}
}
运行程序,输出:
程序开始启动...
程序即将退出...
开始执行钩子方法...
如果程序使用 Java Security Managers,使用钩子方法则需要安全权限 RuntimePermission(“shutdownHooks”),否则会导致 SecurityException。
不能在钩子方法中调用 System.exit(),否则卡住 JVM 的关闭过程,但是可以调用 Runtime.halt() 方法。
Hook 线程中同样会抛出异常,对于未捕捉的异常,线程的默认异常处理器处理该异常,不会影响其他 Hook 线程以及 JVM 正常退出。
使用案例
关闭钩子在 Spring 中的运用
关闭钩子在 Spring 中是如何运用的呢。通过源码分析,Spring Boot 项目启动时会判断 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);
}
}
转载
作者:殷建卫 链接:https://www.yuque.com/yinjianwei/vyrvkf/gnekbd 来源:殷建卫 - 开发笔记 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。