Java Shutdown Hook 场景使用和源码分析
} 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())
《一线大厂 Java 面试题解析+后端开发学习笔记+最新架构讲解视频+实战项目源码讲义》开源 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.lambda1(ShutdownHookDemo.java:8)
at java.lang.Thread.run(Thread.java:748)
如果调用 Runtime.getRuntime().halt()
方法停止 JVM,那么虚拟机是不会调用钩子的。
package c Java 开源项目【ali1024.coding.net/public/P7/Java/git】 om.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);
读者福利
分享一份自己整理好的 Java 面试手册,还有一些面试题 pdf
不要停下自己学习的脚步
评论