ShutdownHook原理
微信搜索“捉虫大师”,点赞、关注是对我最大的鼓励
ShutdownHook介绍
在java程序中,很容易在进程结束时添加一个钩子,即ShutdownHook。通常在程序启动时加入以下代码即可
Runtime.getRuntime().addShutdownHook(new Thread(){
@Override
public void run() {
System.out.println("I'm shutdown hook...");
}
});
有了ShutdownHook我们可以
- 在进程结束时做一些善后工作,例如释放占用的资源,保存程序状态等
- 为优雅(平滑)发布提供手段,在程序关闭前摘除流量
不少java中间件或框架都使用了ShutdownHook的能力,如dubbo、spring等。
spring中在application context被load时会注册一个ShutdownHook。
这个ShutdownHook会在进程退出前执行销毁bean,发出ContextClosedEvent等动作。
而dubbo在spring框架下正是监听了ContextClosedEvent,调用dubboBootstrap.stop()来实现清理现场和dubbo的优雅发布,spring的事件机制默认是同步的,所以能在publish事件时等待所有监听者执行完毕。
ShutdownHook原理
ShutdownHook的数据结构与执行顺序
- 当我们添加一个ShutdownHook时,会调用
ApplicationShutdownHooks.add(hook),往ApplicationShutdownHooks类下的静态变量private static IdentityHashMap<Thread, Thread> hooks添加一个hook,hook本身是一个thread对象 ApplicationShutdownHooks类初始化时会把hooks添加到Shutdown的hooks中去,而Shutdown的hooks是系统级的ShutdownHook,并且系统级的ShutdownHook由一个数组构成,只能添加10个- 系统级的ShutdownHook调用了thread类的
run方法,所以系统级的ShutdownHook是同步有序执行的
private static void runHooks() {
for (int i=0; i < MAX_SYSTEM_HOOKS; i++) {
try {
Runnable hook;
synchronized (lock) {
// acquire the lock to make sure the hook registered during
// shutdown is visible here.
currentRunningHook = i;
hook = hooks[i];
}
if (hook != null) hook.run();
} catch(Throwable t) {
if (t instanceof ThreadDeath) {
ThreadDeath td = (ThreadDeath)t;
throw td;
}
}
}
}
- 系统级的ShutdownHook的
add方法是包可见,即我们不能直接调用它 ApplicationShutdownHooks位于下标1处,且应用级的hooks,执行时调用的是thread类的start方法,所以应用级的ShutdownHook是异步执行的,但会等所有hook执行完毕才会退出。
static void runHooks() {
Collection<Thread> threads;
synchronized(ApplicationShutdownHooks.class) {
threads = hooks.keySet();
hooks = null;
}
for (Thread hook : threads) {
hook.start();
}
for (Thread hook : threads) {
while (true) {
try {
hook.join();
break;
} catch (InterruptedException ignored) {
}
}
}
}
用一副图总结如下:

ShutdownHook触发点
从Shutdown的runHooks顺藤摸瓜,我们得出以下这个调用路径

重点看Shutdown.exit 和 Shutdown.shutdown
Shutdown.exit
跟进Shutdown.exit的调用方,发现有 Runtime.exit 和 Terminator.setup
Runtime.exit是代码中主动结束进程的接口Terminator.setup被initializeSystemClass调用,当第一个线程被初始化的时候被触发,触发后注册了一个信号监控函数,捕获kill发出的信号,调用Shutdown.exit结束进程
这样覆盖了代码中主动结束进程和被kill杀死进程的场景。
主动结束进程不必介绍,这里说一下信号捕获。在java中我们可以写出如下代码来捕获kill信号,只需要实现SignalHandler接口以及handle方法,程序入口处注册要监听的相应信号即可,当然不是每个信号都能捕获处理。
public class SignalHandlerTest implements SignalHandler {
public static void main(String[] args) {
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
System.out.println("I'm shutdown hook ");
}
});
SignalHandler sh = new SignalHandlerTest();
Signal.handle(new Signal("HUP"), sh);
Signal.handle(new Signal("INT"), sh);
//Signal.handle(new Signal("QUIT"), sh);// 该信号不能捕获
Signal.handle(new Signal("ABRT"), sh);
//Signal.handle(new Signal("KILL"), sh);// 该信号不能捕获
Signal.handle(new Signal("ALRM"), sh);
Signal.handle(new Signal("TERM"), sh);
while (true) {
System.out.println("main running");
try {
Thread.sleep(2000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
@Override
public void handle(Signal signal) {
System.out.println("receive signal " + signal.getName() + "-" + signal.getNumber());
System.exit(0);
}
}
要注意的是通常来说,我们捕获信号,做了一些个性化的处理后需要主动调用System.exit,否则进程就不会退出了,这时只能使用kill -9来强制杀死进程了。
而且每次信号的捕获是在不同的线程中,所以他们之间的执行是异步的。
Shutdown.shutdown
这个方法可以看注释
/* Invoked by the JNI DestroyJavaVM procedure when the last non-daemon
* thread has finished. Unlike the exit method, this method does not
* actually halt the VM.
*/
翻译一下就是该方法会在最后一个非daemon线程(非守护线程)结束时被JNI的DestroyJavaVM方法调用。
java中有两类线程,用户线程和守护线程,守护线程是服务于用户线程,如GC线程,JVM判断是否结束的标志就是是否还有用户线程在工作。
当最后一个用户线程结束时,就会调用 Shutdown.shutdown。这是JVM这类虚拟机语言特有的"权利",倘若是golang这类编译成可执行的二进制文件时,当全部用户线程结束时是不会执行ShutdownHook的。
举个例子,当java进程正常退出时,没有在代码中主动结束进程,也没有kill,就像这样
public static void main(String[] args) {
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
super.run();
System.out.println("I'm shutdown hook ");
}
});
}
当main线程运行完了后,也能打印出I'm shutdown hook,反观golang就做不到这一点(如果可以做到,可以私信告诉我,我是个golang新手)
通过如上两个调用的分析,我们概括出如下结论:

我们能看出java的ShutdownHook其实覆盖的非常全面了,只有一处无法覆盖,即当我们杀死进程时使用了kill -9时,由于程序无法捕获处理,进程被直接杀死,所以无法执行ShutdownHook。
总结
综上,我们得出一些结论
- 重写捕获信号需要注意主动退出进程,否则进程可能永远不会退出,捕获信号的执行是异步的
- 用户级的ShutdownHook是绑定在系统级的ShutdownHook之上,且用户级是异步执行,系统级是同步顺序执行,用户级处于系统级执行顺序的第二位
- ShutdownHook 覆盖的面比较广,不论是手动调用接口退出进程,还是捕获信号退出进程,抑或是用户线程执行完毕退出,都会执行ShutdownHook,唯一不会执行的就是kill -9
关于作者:公众号"捉虫大师"作者,专注后端的中间件开发,关注我,给推送你最纯粹的技术干货
ShutdownHook原理的更多相关文章
- 排查dubbo接口重复注销问题,我发现了一个巧妙的设计
背景 我在公司内负责自研的dubbo注册中心相关工作,群里经常接到业务方反馈dubbo接口注销报错.经排查,确定是同一个接口调用了两次注销接口导致,由于我们的注册中心注销接口不能重复调用,调用第二次会 ...
- rocketmq优雅停机往事
1 时间追溯到2018年12月的某一天夜晚,那天我正准备上线一个需求完就回家,刚点下发布按钮,告警就响起,我擦,难道回不了家了?看着报错量只有一两个,断定只是偶发,稳住不要慌. 把剩下的机器发完,又出 ...
- Java 技术栈中间件优雅停机方案设计与实现全景图
欢迎关注公众号:bin的技术小屋,阅读公众号原文 本系列 Netty 源码解析文章基于 4.1.56.Final 版本 本文概要 在上篇文章 我为 Netty 贡献源码 | 且看 Netty 如何应对 ...
- Tomcat服务器原理详解
[目录]本文主要讲解Tomcat启动和部署webapp时的原理和过程,以及其使用的配置文件的详解.主要有三大部分: 第一部分.Tomcat的简介和启动过程 第二部分.Tomcat部署webapp 第三 ...
- 定时组件quartz系列<二>quartz的集群原理
1.基本信息: Quartz是一个开源的作业调度框架,它完全由java写成,并设计用于J2Se和J2EE应用中.它提供了巨大的灵活性而不牺牲简单性.你能够用它 来为执行一个作业而创建简单的或 ...
- springboot之启动原理解析
前言 SpringBoot为我们做的自动配置,确实方便快捷,但是对于新手来说,如果不大懂SpringBoot内部启动原理,以后难免会吃亏.所以这次博主就跟你们一起一步步揭开SpringBoot的神秘面 ...
- SpringBoot启动原理及相关流程
一.springboot启动原理及相关流程概览 springboot是基于spring的新型的轻量级框架,最厉害的地方当属自动配置.那我们就可以根据启动流程和相关原理来看看,如何实现传奇的自动配置 二 ...
- spring boot(二):启动原理解析
我们开发任何一个Spring Boot项目,都会用到如下的启动类 @SpringBootApplication public class Application { public static voi ...
- 哦,这就是java的优雅停机?(实现及原理)
优雅停机? 这个名词我是服的,如果抛开专业不谈,多好的名词啊! 其实优雅停机,就是在要关闭服务之前,不是立马全部关停,而是做好一些善后操作,比如:关闭线程.释放连接资源等. 再比如,就是不会让调用方的 ...
随机推荐
- 小程序 mpvue page "xxx" has not been registered yet
新增了几个页面,改了下目录结构,就开始报这个错. 重启了几次不管用,google 一番也无果. 灵机一动试一下 build npm run build build 版本没报错,OK 然后 $ rm - ...
- C# AutoMaper简单使用
AutoMapper它是一种对象与对象之间的映射器,让AutoMapper有意思的就是在于它提供了一些将类型A映射到类型B这种无聊的实例,只要B遵循AutoMapper已经建立的惯例,那么大多数情况下 ...
- CSS001. 纯CSS实现瀑布流(纵向排序)
通过 Multi-columns 相关的属性 column-count.column-gap 配合 break-inside 来实现瀑布流布局. 首先对包裹图片的盒子增加样式,column-count ...
- Mybatis-基本学习(上)
目录 Mybatis mybatis开始 -----环境准备 一.简介 1.什么是MyBatis 2.持久化 3.持久层 4.为什么需要Mybatis? 二.第一个Mybatis程序 1.搭建环境 1 ...
- Redis核心原理与实践--列表实现原理之ziplist
列表类型可以存储一组按插入顺序排序的字符串,它非常灵活,支持在两端插入.弹出数据,可以充当栈和队列的角色. > LPUSH fruit apple (integer) 1 > RPUSH ...
- Java学习笔记--常用容器
容器 1. 出现原因 解决程序运行时需要创建新对象,在程序运行前不知道运行的所需的对象数量甚至是类型的问题. Java中提供了一套集合类来解决这些问题包括:List.Set.Queue.Map 2. ...
- idea创建Maven项目没有src目录,且依赖也没有更新
刚开始用idea的时候,重新配置安装了Maven,但是创建项目的时候发现创建的目录少了很多东西,今天重新查看了一下,发现了原因....... 话不多说,安装配置Maven的方法网上都有,我之前改路径的 ...
- PHP中PDO关闭连接的问题
在之前我们手写 mysql 的连接操作时,一般都会使用 mysql_close() 来进行关闭数据库连接的操作.不过在现代化的开发中,一般使用框架都会让我们忽视了底层的这些封装,而且大部分框架都已经默 ...
- 对象赋值在PHP中到底是不是引用?
之前的文章中,我们说过变量赋值的问题,其中有一个问题是对象在进行变量赋值的时候,直接就是引用赋值.那么到底真实情况是怎样呢? 之前变量赋值的文章 PHP的变量赋值 对象引用测试 在继续深入的学习PHP ...
- pypandoc库实现文档转换
写在前面: 对于python程序员来说,文件格式之间转换很常用,尤其是把我们爬虫爬到的内容转换成想要的文档格式时.这几天看到一个网站上有许多文章,个人很喜欢,直接复制太麻烦,为了将爬到的html文件以 ...