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

作者:陈皮的JavaLib 时间:2023-05-19 06:01:30 

目录
  • 背景

  • Shutdown Hook 介绍

  • 关闭钩子被调用场景

  • 注意事项

  • 实践

  • Shutdown Hook 在 Spring 中的运用

背景

如果想在 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) {
// 省略
}

关闭钩子被调用场景

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

  • 程序正常退出

  • 程序调用 System.exit() 退出

  • 终端使用 Ctrl+C 中断程序

  • 程序抛出异常导致程序退出,例如 OOM,数组越界等异常

  • 系统事件,例如用户注销或关闭系统

  • 使用 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秒让所有任务执行完成。

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

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);
}
}

来源:https://www.cnblogs.com/luciochn/p/14878160.html

标签:Java,Shutdown,Hook
0
投稿

猜你喜欢

  • java实现哈弗曼编码与反编码实例分享(哈弗曼算法)

    2023-11-25 04:54:05
  • 如何用Java Stream写出既高雅又装*的代码

    2022-04-13 23:23:58
  • Java实现几种序列化方式总结

    2023-02-13 06:18:27
  • java @Value(

    2023-10-05 02:54:47
  • MybatisPlus #{param}和${param}的用法详解

    2023-02-02 13:08:10
  • Java中ArrayList与顺序表的定义与实现方法

    2022-06-08 03:27:12
  • Android实现带描边的圆角图片

    2021-09-06 19:00:04
  • Android实现价格走势自定义曲线图

    2023-04-03 23:15:06
  • java实现航空用户管理系统

    2023-11-24 02:42:37
  • Spring @Async无法实现异步的解决方案

    2021-10-22 13:32:46
  • Java比较问题详细分析

    2023-11-20 14:30:48
  • Android笔记之:CM9源码下载与编译的应用

    2022-09-23 05:40:14
  • springboot添加https服务器的方法

    2022-08-19 06:14:31
  • 基于Java向zip压缩包追加文件

    2023-10-11 17:18:52
  • Android中Canvas的常用方法总结

    2021-11-25 03:00:28
  • Springboot 整合shiro实现权限控制的方法

    2021-09-21 20:15:47
  • 解决C#中取消方向键对控件焦点控制的实现方法

    2021-09-13 08:02:29
  • java开发微信分享接口的步骤

    2021-08-22 12:30:59
  • Java实现单例模式的五种方法介绍

    2022-10-20 17:32:56
  • C#子类对基类方法的继承、重写与隐藏详解

    2023-01-31 04:48:46
  • asp之家 软件编程 m.aspxhome.com