我是陈皮,一个在互联网 Coding 的 ITer,微信搜索「陈皮的JavaLib」第一时间阅读最新文章,回复【资料】,即可获得我精心整理的技术资料,电子书籍,一线大厂面试资料和优秀简历模板。

如果想在 Java 进程退出时,包括正常和异常退出,做一些额外处理工作,例如资源清理,对象销毁,内存数据持久化到磁盘,等待线程池处理完所有任务等等。特别是进程异常挂掉的情况,如果一些重要状态没及时保留下来,或线程池的任务没被处理完,有可能会造成严重问题。那该怎么办呢?

Java 中的 Shutdown Hook 提供了比较好的方案。我们可以通过 Java.Runtime.addShutdownHook(Thread hook) 方法向 JVM 注册关闭钩子,在 JVM 退出之前会自动调用执行钩子方法,做一些结尾操作,从而让进程平滑优雅的退出,保证了业务的完整性。

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

在虚拟机关闭的过程中,还可以继续注册新的钩子,或者撤销已经注册过的钩子。不过有可能会抛出 IllegalStateException。注册和注销钩子的方法定义如下:

  1. public void addShutdownHook(Thread hook) {
  2. // 省略
  3. }
  4. public void removeShutdownHook(Thread hook) {
  5. // 省略
  6. }

关闭钩子可以在以下几种场景被调用:

  1. 程序正常退出
  2. 程序调用 System.exit() 退出
  3. 终端使用 Ctrl+C 中断程序
  4. 程序抛出异常导致程序退出,例如 OOM,数组越界等异常
  5. 系统事件,例如用户注销或关闭系统
  6. 使用 Kill pid 命令杀掉进程,注意使用 kill -9 pid 强制杀掉不会触发执行钩子

验证程序正常退出情况

  1. package com.chenpi;
  2. public class ShutdownHookDemo {
  3. static {
  4. Runtime.getRuntime().addShutdownHook(new Thread(() -> System.out.println("执行钩子方法...")));
  5. }
  6. public static void main(String[] args) throws InterruptedException {
  7. System.out.println("程序开始启动...");
  8. Thread.sleep(2000);
  9. System.out.println("程序即将退出...");
  10. }
  11. }

运行结果

  1. 程序开始启动...
  2. 程序即将退出...
  3. 执行钩子方法...
  4. Process finished with exit code 0

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

  1. package com.chenpi;
  2. public class ShutdownHookDemo {
  3. static {
  4. Runtime.getRuntime().addShutdownHook(new Thread(() -> System.out.println("执行钩子方法...")));
  5. }
  6. public static void main(String[] args) throws InterruptedException {
  7. System.out.println("程序开始启动...");
  8. Thread.sleep(2000);
  9. System.exit(-1);
  10. System.out.println("程序即将退出...");
  11. }
  12. }

运行结果

  1. 程序开始启动...
  2. 执行钩子方法...
  3. Process finished with exit code -1

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

  1. package com.chenpi;
  2. public class ShutdownHookDemo {
  3. static {
  4. Runtime.getRuntime().addShutdownHook(new Thread(() -> System.out.println("执行钩子方法...")));
  5. }
  6. public static void main(String[] args) throws InterruptedException {
  7. System.out.println("程序开始启动...");
  8. Thread.sleep(2000);
  9. System.out.println("程序即将退出...");
  10. }
  11. }

运行结果

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

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

  1. package com.chenpi;
  2. public class ShutdownHookDemo {
  3. static {
  4. Runtime.getRuntime().addShutdownHook(new Thread(() -> System.out.println("执行钩子方法...")));
  5. }
  6. public static void main(String[] args) {
  7. System.out.println("程序开始启动...");
  8. int a = 0;
  9. System.out.println(10 / a);
  10. System.out.println("程序即将退出...");
  11. }
  12. }

运行结果

  1. 程序开始启动...
  2. 执行钩子方法...
  3. Exception in thread "main" java.lang.ArithmeticException: / by zero
  4. at com.chenpi.ShutdownHookDemo.main(ShutdownHookDemo.java:12)
  5. Process finished with exit code 1

至于系统被关闭,或者使用 Kill pid 命令杀掉进程就不演示了,感兴趣的可以自行验证。

可以向虚拟机注册多个关闭钩子,但是注意这些钩子执行是并发的,执行顺序是不确定的。

  1. package com.chenpi;
  2. public class ShutdownHookDemo {
  3. static {
  4. Runtime.getRuntime().addShutdownHook(new Thread(() -> System.out.println("执行钩子方法A...")));
  5. Runtime.getRuntime().addShutdownHook(new Thread(() -> System.out.println("执行钩子方法B...")));
  6. Runtime.getRuntime().addShutdownHook(new Thread(() -> System.out.println("执行钩子方法C...")));
  7. }
  8. public static void main(String[] args) throws InterruptedException {
  9. System.out.println("程序开始启动...");
  10. Thread.sleep(2000);
  11. System.out.println("程序即将退出...");
  12. }
  13. }

运行结果

  1. 程序开始启动...
  2. 程序即将退出...
  3. 执行钩子方法B...
  4. 执行钩子方法C...
  5. 执行钩子方法A...

向虚拟机注册的钩子方法需要尽快执行结束,尽量不要执行长时间的操作,例如 I/O 等可能被阻塞的操作,死锁等,这样就会导致程序短时间不能被关闭,甚至一直关闭不了。我们也可以引入超时机制强制退出钩子,让程序正常结束。

  1. package com.chenpi;
  2. public class ShutdownHookDemo {
  3. static {
  4. Runtime.getRuntime().addShutdownHook(new Thread(() -> {
  5. // 模拟长时间的操作
  6. try {
  7. Thread.sleep(1000000);
  8. } catch (InterruptedException e) {
  9. e.printStackTrace();
  10. }
  11. }));
  12. }
  13. public static void main(String[] args) throws InterruptedException {
  14. System.out.println("程序开始启动...");
  15. Thread.sleep(2000);
  16. System.out.println("程序即将退出...");
  17. }
  18. }

以上的钩子执行时间比较长,最终会导致程序在等待很长时间之后才能被关闭。

如果 JVM 已经调用执行关闭钩子的过程中,不允许注册新的钩子和注销已经注册的钩子,否则会报 IllegalStateException 异常。通过源码分析,JVM 调用钩子的时候,即调用 ApplicationShutdownHooks#runHooks() 方法,会将所有钩子从变量 hooks 取出,然后将此变量置为 null

  1. // 调用执行钩子
  2. static void runHooks() {
  3. Collection<Thread> threads;
  4. synchronized(ApplicationShutdownHooks.class) {
  5. threads = hooks.keySet();
  6. hooks = null;
  7. }
  8. for (Thread hook : threads) {
  9. hook.start();
  10. }
  11. for (Thread hook : threads) {
  12. try {
  13. hook.join();
  14. } catch (InterruptedException x) { }
  15. }
  16. }

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

  1. // 注册钩子
  2. static synchronized void add(Thread hook) {
  3. if(hooks == null)
  4. throw new IllegalStateException("Shutdown in progress");
  5. if (hook.isAlive())
  6. throw new IllegalArgumentException("Hook already running");
  7. if (hooks.containsKey(hook))
  8. throw new IllegalArgumentException("Hook previously registered");
  9. hooks.put(hook, hook);
  10. }
  11. // 注销钩子
  12. static synchronized boolean remove(Thread hook) {
  13. if(hooks == null)
  14. throw new IllegalStateException("Shutdown in progress");
  15. if (hook == null)
  16. throw new NullPointerException();
  17. return hooks.remove(hook) != null;
  18. }

我们演示下这种情况

  1. package com.chenpi;
  2. public class ShutdownHookDemo {
  3. static {
  4. Runtime.getRuntime().addShutdownHook(new Thread(() -> {
  5. System.out.println("执行钩子方法...");
  6. Runtime.getRuntime().addShutdownHook(new Thread(
  7. () -> System.out.println("在JVM调用钩子的过程中再新注册钩子,会报错IllegalStateException")));
  8. // 在JVM调用钩子的过程中注销钩子,会报错IllegalStateException
  9. Runtime.getRuntime().removeShutdownHook(Thread.currentThread());
  10. }));
  11. }
  12. public static void main(String[] args) throws InterruptedException {
  13. System.out.println("程序开始启动...");
  14. Thread.sleep(2000);
  15. System.out.println("程序即将退出...");
  16. }
  17. }

运行结果

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

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

  1. package com.chenpi;
  2. public class ShutdownHookDemo {
  3. static {
  4. Runtime.getRuntime().addShutdownHook(new Thread(() -> System.out.println("执行钩子方法...")));
  5. }
  6. public static void main(String[] args) {
  7. System.out.println("程序开始启动...");
  8. System.out.println("程序即将退出...");
  9. Runtime.getRuntime().halt(0);
  10. }
  11. }

运行结果

  1. 程序开始启动...
  2. 程序即将退出...
  3. Process finished with exit code 0

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

  1. package com.chenpi;
  2. public class ShutdownHookDemo {
  3. static {
  4. Runtime.getRuntime().addShutdownHook(new Thread(() -> {
  5. System.out.println("开始执行钩子方法...");
  6. Runtime.getRuntime().halt(-1);
  7. System.out.println("结束执行钩子方法...");
  8. }));
  9. }
  10. public static void main(String[] args) {
  11. System.out.println("程序开始启动...");
  12. System.out.println("程序即将退出...");
  13. }
  14. }

运行结果

  1. 程序开始启动...
  2. 程序即将退出...
  3. 开始执行钩子方法...
  4. Process finished with exit code -1

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

例如,我们程序自定义了一个线程池,用来接收和处理任务。如果程序突然奔溃异常退出,这时线程池的所有任务有可能还未处理完成,如果不处理完程序就直接退出,可能会导致数据丢失,业务异常等重要问题。这时钩子就派上用场了。

  1. import java.util.concurrent.ExecutorService;
  2. import java.util.concurrent.Executors;
  3. import java.util.concurrent.TimeUnit;
  4. public class ShutdownHookDemo {
  5. // 线程池
  6. private static ExecutorService executorService = Executors.newFixedThreadPool(3);
  7. static {
  8. Runtime.getRuntime().addShutdownHook(new Thread(() -> {
  9. System.out.println("开始执行钩子方法...");
  10. // 关闭线程池
  11. executorService.shutdown();
  12. try {
  13. // 等待60秒
  14. System.out.println(executorService.awaitTermination(60, TimeUnit.SECONDS));
  15. } catch (InterruptedException e) {
  16. e.printStackTrace();
  17. }
  18. System.out.println("结束执行钩子方法...");
  19. }));
  20. }
  21. public static void main(String[] args) throws InterruptedException {
  22. System.out.println("程序开始启动...");
  23. // 向线程池添加10个任务
  24. for (int i = 0; i < 10; i++) {
  25. Thread.sleep(1000);
  26. final int finalI = i;
  27. executorService.execute(() -> {
  28. try {
  29. Thread.sleep(4000);
  30. } catch (InterruptedException e) {
  31. e.printStackTrace();
  32. }
  33. System.out.println("Task " + finalI + " execute...");
  34. });
  35. System.out.println("Task " + finalI + " is in thread pool...");
  36. }
  37. }
  38. }

在命令行窗口中运行程序,在10个任务都提交到线程池之后,任务都还未处理完成之前,使用 Ctrl+C 中断程序,最终在虚拟机关闭之前,调用了关闭钩子,关闭线程池,并且等待60秒让所有任务执行完成。

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

  1. private void refreshContext(ConfigurableApplicationContext context) {
  2. refresh(context);
  3. if (this.registerShutdownHook) {
  4. try {
  5. context.registerShutdownHook();
  6. }
  7. catch (AccessControlException ex) {
  8. // Not allowed in some environments.
  9. }
  10. }
  11. }
  12. @Override
  13. public void registerShutdownHook() {
  14. if (this.shutdownHook == null) {
  15. // No shutdown hook registered yet.
  16. this.shutdownHook = new Thread() {
  17. @Override
  18. public void run() {
  19. synchronized (startupShutdownMonitor) {
  20. // 钩子方法
  21. doClose();
  22. }
  23. }
  24. };
  25. // 底层还是使用此方法注册钩子
  26. Runtime.getRuntime().addShutdownHook(this.shutdownHook);
  27. }
  28. }

在关闭钩子的方法 doClose 中,会做一些虚拟机关闭前处理工作,例如销毁容器里所有单例 Bean,关闭 BeanFactory,发布关闭事件等等。

  1. protected void doClose() {
  2. // Check whether an actual close attempt is necessary...
  3. if (this.active.get() && this.closed.compareAndSet(false, true)) {
  4. if (logger.isDebugEnabled()) {
  5. logger.debug("Closing " + this);
  6. }
  7. LiveBeansView.unregisterApplicationContext(this);
  8. try {
  9. // 发布Spring 应用上下文的关闭事件,让监听器在应用关闭之前做出响应处理
  10. publishEvent(new ContextClosedEvent(this));
  11. }
  12. catch (Throwable ex) {
  13. logger.warn("Exception thrown from ApplicationListener handling ContextClosedEvent", ex);
  14. }
  15. // Stop all Lifecycle beans, to avoid delays during individual destruction.
  16. if (this.lifecycleProcessor != null) {
  17. try {
  18. // 执行lifecycleProcessor的关闭方法
  19. this.lifecycleProcessor.onClose();
  20. }
  21. catch (Throwable ex) {
  22. logger.warn("Exception thrown from LifecycleProcessor on context close", ex);
  23. }
  24. }
  25. // 销毁容器里所有单例Bean
  26. destroyBeans();
  27. // 关闭BeanFactory
  28. closeBeanFactory();
  29. // Let subclasses do some final clean-up if they wish...
  30. onClose();
  31. // Reset local application listeners to pre-refresh state.
  32. if (this.earlyApplicationListeners != null) {
  33. this.applicationListeners.clear();
  34. this.applicationListeners.addAll(this.earlyApplicationListeners);
  35. }
  36. // Switch to inactive.
  37. this.active.set(false);
  38. }
  39. }

我们知道,我们可以定义 bean 并且实现 DisposableBean 接口,重写 destroy 对象销毁方法。destroy 方法就是在 Spring 注册的关闭钩子里被调用的。例如我们使用 Spring 框架的 ThreadPoolTaskExecutor 线程池类,它就实现了 DisposableBean 接口,重写了 destroy 方法,从而在程序退出前,进行线程池销毁工作。源码如下:

  1. @Override
  2. public void destroy() {
  3. shutdown();
  4. }
  5. /**
  6. * Perform a shutdown on the underlying ExecutorService.
  7. * @see java.util.concurrent.ExecutorService#shutdown()
  8. * @see java.util.concurrent.ExecutorService#shutdownNow()
  9. */
  10. public void shutdown() {
  11. if (logger.isInfoEnabled()) {
  12. logger.info("Shutting down ExecutorService" + (this.beanName != null ? " '" + this.beanName + "'" : ""));
  13. }
  14. if (this.executor != null) {
  15. if (this.waitForTasksToCompleteOnShutdown) {
  16. this.executor.shutdown();
  17. }
  18. else {
  19. for (Runnable remainingTask : this.executor.shutdownNow()) {
  20. cancelRemainingTask(remainingTask);
  21. }
  22. }
  23. awaitTerminationIfNecessary(this.executor);
  24. }
  25. }

版权声明:本文为luciochn原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/luciochn/p/14878160.html