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

背景

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

关闭钩子被调用场景

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

  1. 程序正常退出
  2. 程序调用 System.exit() 退出
  3. 终端使用 Ctrl+C 中断程序
  4. 程序抛出异常导致程序退出,例如 OOM,数组越界等异常
  5. 系统事件,例如用户注销或关闭系统
  6. 使用 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秒让所有任务执行完成。

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

Java Shutdown Hook 场景使用和源码分析的更多相关文章

  1. java集合框架04——LinkedList和源码分析

    上一章学习了ArrayList,并分析了其源码,这一章我们将对LinkedList的具体实现进行详细的学习.依然遵循上一章的步骤,先对LinkedList有个整体的认识,然后学习它的源码,深入剖析Li ...

  2. java集合框架03——ArrayList和源码分析

    最近忙着替公司招人好久没写了,荒废了不好意思. 上一章学习了Collection的架构,并阅读了部分源码,这一章开始,我们将对Collection的具体实现进行详细学习.首先学习List.而Array ...

  3. Java并发编程(七)ConcurrentLinkedQueue的实现原理和源码分析

    相关文章 Java并发编程(一)线程定义.状态和属性 Java并发编程(二)同步 Java并发编程(三)volatile域 Java并发编程(四)Java内存模型 Java并发编程(五)Concurr ...

  4. Java线程池使用和源码分析

    1.为什么使用线程池 在多线程编程中一项很重要的功能就是执行任务,而执行任务的方式有很多种,为什么一定需要使用线程池呢?下面我们使用Socket编程处理请求的功能,分别对每种执行任务的方式进行分析. ...

  5. Quartz学习--二 Hello Quartz! 和源码分析

    Quartz学习--二  Hello Quartz! 和源码分析 三.  Hello Quartz! 我会跟着 第一章 6.2 的图来 进行同步代码编写 简单入门示例: 创建一个新的java普通工程 ...

  6. Kubernetes Job Controller 原理和源码分析(三)

    概述Job controller 的启动processNextWorkItem()核心调谐逻辑入口 - syncJob()Pod 数量管理 - manageJob()小结 概述 源码版本:kubern ...

  7. Kubernetes Job Controller 原理和源码分析(一)

    概述什么是 JobJob 入门示例Job 的 specPod Template并发问题其他属性 概述 Job 是主要的 Kubernetes 原生 Workload 资源之一,是在 Kubernete ...

  8. Kubernetes Job Controller 原理和源码分析(二)

    概述程序入口Job controller 的创建Controller 对象NewController()podControlEventHandlerJob AddFunc DeleteFuncJob ...

  9. 消息队列的一些场景及源码分析,RocketMQ使用相关问题及性能优化

    前文目录链接参考: 消息队列的一些场景及源码分析,RocketMQ使用相关问题及性能优化 https://www.cnblogs.com/yizhiamumu/p/16694126.html 消息队列 ...

随机推荐

  1. POJ2337 欧拉路径字典序输出

    题意:       给一些单词,问是否可以每个单词只用一次,然后连接在一起(不一定要成环,能连接在一起就行). 思路:       这个题目的入手点比较好想,其实就是问欧拉路径,先说下解题步骤,然后在 ...

  2. 将一个eclipse的SSM项目用IDEA打开并运行

    项目部署 将一个eclipse项目用idea打开,并且 部署到tomcat中 .或者你tomcat部署成功,但是启动就是404,下面的步骤就要更认真看了 项目配置 打开idea,Import Proj ...

  3. mac打开class文件

    本来不想写这个东西的.但是这个却费了我一番周折. 我要先声明一点的是,我从来不讲iOS当成一个单独的系统,而是将这个操作系统归位unix内核的系统. 简单来说,我把它当成linux在用. 但是,mac ...

  4. 游戏中的2D OBB碰撞模型的碰撞算法介绍和实践

    前言 上一篇博文说道,射线与场景中模型上的所有三角形求交时,会大幅度影响效率且花费比较多的时间,因此会采取使用包围盒的形式,进行一个加速求交.在此文中介绍OBB碰撞模型的碰撞算法 OBB的碰撞模型 有 ...

  5. SparkSQL电商用户画像(三)之环境准备

    五. 电商用户画像环境搭建 众所周知,Hive的执行任务是将hql语句转化为MapReduce来计算的,Hive的整体解决方案很不错,但是从查询提交到结果返回需要相当长的时间,查询耗时太长.这个主要原 ...

  6. ArcGIS JS API使用PrintTask打印地图问题解决汇总

    环境:来源于工作过程,使用的API是  arcgis js 3.*  3系API,4.*暂时没测试: 1.数据与打印服务跨域情况下,不能打印问题. 一般情况下,我们发布的数据服务和打印服务是在一台服务 ...

  7. 深入源码理解SpringBean生命周期

    概述 本文描述下Spring的实例化.初始化.销毁,整个SpringBean生命周期,聊一聊BeanPostProcessor的回调时机.Aware方法的回调时机.初始化方法的回调及其顺序.销毁方法的 ...

  8. (转)通过gitlab统计git提交的代码量

    git的代码量大多数都是根据命令行统计,或者根据第三方插件统计.但是都不满足我的需求,因为我们代码都由gitlab管理,于是想到了通过gitlab暴露出来的接口获取数据. 第一步,生成私钥 登录你的g ...

  9. BUA软件工程个人博客作业

    写在前面 项目 内容 所属课程 2020春季计算机学院软件工程(罗杰 任健) (北航) 作业要求 个人博客作业 课程目标 培养软件开发能力 本作业对实现目标的具体作用 阅读教材,了解软件工程,并比较各 ...

  10. opencv——轮廓发现与轮廓(二值图像)分析

    引言 二值图像分析最常见的一个主要方式就是轮廓发现与轮廓分析,其中轮廓发现的目的是为轮廓分析做准备,经过轮廓分析我们可以得到轮廓各种有用的属性信息. 这里顺带提下边缘检测,和轮廓提取的区别: 边缘检测 ...