写点什么

Java Shutdown Hook 场景使用和源码分析

  • 2022 年 4 月 19 日
  • 本文字数:3304 字

    阅读完需:约 11 分钟

} 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


不要停下自己学习的脚步




用户头像

还未添加个人签名 2022.04.13 加入

还未添加个人简介

评论

发布
暂无评论
Java Shutdown Hook 场景使用和源码分析_Java_爱好编程进阶_InfoQ写作平台