微信搜索“捉虫大师”,点赞、关注是对我最大的鼓励

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添加到Shutdownhooks中去,而Shutdownhooks是系统级的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触发点

ShutdownrunHooks顺藤摸瓜,我们得出以下这个调用路径

重点看Shutdown.exitShutdown.shutdown

Shutdown.exit

跟进Shutdown.exit的调用方,发现有 Runtime.exitTerminator.setup

  • Runtime.exit 是代码中主动结束进程的接口
  • Terminator.setupinitializeSystemClass 调用,当第一个线程被初始化的时候被触发,触发后注册了一个信号监控函数,捕获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原理的更多相关文章

  1. 排查dubbo接口重复注销问题,我发现了一个巧妙的设计

    背景 我在公司内负责自研的dubbo注册中心相关工作,群里经常接到业务方反馈dubbo接口注销报错.经排查,确定是同一个接口调用了两次注销接口导致,由于我们的注册中心注销接口不能重复调用,调用第二次会 ...

  2. rocketmq优雅停机往事

    1 时间追溯到2018年12月的某一天夜晚,那天我正准备上线一个需求完就回家,刚点下发布按钮,告警就响起,我擦,难道回不了家了?看着报错量只有一两个,断定只是偶发,稳住不要慌. 把剩下的机器发完,又出 ...

  3. Java 技术栈中间件优雅停机方案设计与实现全景图

    欢迎关注公众号:bin的技术小屋,阅读公众号原文 本系列 Netty 源码解析文章基于 4.1.56.Final 版本 本文概要 在上篇文章 我为 Netty 贡献源码 | 且看 Netty 如何应对 ...

  4. Tomcat服务器原理详解

    [目录]本文主要讲解Tomcat启动和部署webapp时的原理和过程,以及其使用的配置文件的详解.主要有三大部分: 第一部分.Tomcat的简介和启动过程 第二部分.Tomcat部署webapp 第三 ...

  5. 定时组件quartz系列<二>quartz的集群原理

    1.基本信息:      Quartz是一个开源的作业调度框架,它完全由java写成,并设计用于J2Se和J2EE应用中.它提供了巨大的灵活性而不牺牲简单性.你能够用它 来为执行一个作业而创建简单的或 ...

  6. springboot之启动原理解析

    前言 SpringBoot为我们做的自动配置,确实方便快捷,但是对于新手来说,如果不大懂SpringBoot内部启动原理,以后难免会吃亏.所以这次博主就跟你们一起一步步揭开SpringBoot的神秘面 ...

  7. SpringBoot启动原理及相关流程

    一.springboot启动原理及相关流程概览 springboot是基于spring的新型的轻量级框架,最厉害的地方当属自动配置.那我们就可以根据启动流程和相关原理来看看,如何实现传奇的自动配置 二 ...

  8. spring boot(二):启动原理解析

    我们开发任何一个Spring Boot项目,都会用到如下的启动类 @SpringBootApplication public class Application { public static voi ...

  9. 哦,这就是java的优雅停机?(实现及原理)

    优雅停机? 这个名词我是服的,如果抛开专业不谈,多好的名词啊! 其实优雅停机,就是在要关闭服务之前,不是立马全部关停,而是做好一些善后操作,比如:关闭线程.释放连接资源等. 再比如,就是不会让调用方的 ...

随机推荐

  1. 分布式消息流平台:不要只想着Kafka,还有Pulsar

    摘要:Pulsar作为一个云原生的分布式消息流平台,越来越频繁地出现在人们的视野中,大有替代Kafka江湖地位的趋势. 本文分享自华为云社区<MRS Pulsar:下一代分布式消息流平台全新发布 ...

  2. MySQL——优化

    MySQL数据库优化: 1.优化角度 安全: 数据可持续性 性能: 数据的高性能访问 2.优化范围(优化顺序---->) (1)存储.主机和操作系统: 主机架构稳定性 I/O规划及配置 swap ...

  3. 加载映射文件几种方式和mapper接口注解执行sql语句

    一.加载映射文件几种方式 二.mapper接口注解执行sql语句 就将xml中的sql语句放到注解的括号中就可以,一般只用于简单的sql语句合适:

  4. linux 下 I/O 多路复用初探

    本文内容整理自B站up主 free-coder 发布的视频:[并发]IO多路复用select/poll/epoll介绍 引入 一般来讲,服务器在处理IO请求(一般指的是socket编程)时,需要对so ...

  5. this关键字的理解

    this理解为:当前对象 或 当前正在创建的对象 this代表所在类的当前对象的引用(地址值),即对象对自己的引用. 备注:意思方法被那个对象调用,方法中的this就代表那个对象.即谁调用,this就 ...

  6. golang isPowerOfTwo判断是否是2的幂

    iota.go   strconv包 func isPowerOfTwo(x int) bool { return x & (x -1) } 了解n&(n-1)的作用如下: n& ...

  7. djang2.1教育平台02

    在次申明,之所以重做这个资料是因为原幕课教程漏洞太多,新手根本没有办法正常照些学习,我凭着老男孩python 课程基础,重做这个教程 ,更改版本为当前最新版本,为了方法以后的人学习,并不是照着原文照  ...

  8. 傻子都能懂的并查集题解——HDU1232畅通工程

    原题内容: Problem Description 某省调查城镇交通状况,得到现有城镇道路统计表,表中列出了每条道路直接连通的城镇.省政府"畅通工程"的目标是使全省任何两个城镇间都 ...

  9. java.lang.NullPointerException: Attempt to invoke virtual method 'int com.example.xxx.Json.NewsBean.getError_code()' on a null object reference错误解决

    AS在运行的过程中出现了错误: java.lang.NullPointerException: Attempt to invoke virtual method 'int com.example.xx ...

  10. 创建一个Orchard Core CMS 应用程序

    开始使用Orchard Core作为NuGet软件包 在本文中,我们将看到使用Orchard Core提供的NuGet包创建CMS Web应用程序是多么容易. 你可以在这里找到Chris Payne写 ...