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

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. proto buffer

    protobuf是一种高效的数据格式,平台无关.语言无关.可扩展,可用于 RPC 系统和持续数据存储系统. protobuf介绍 Protobuf是Protocol Buffer的简称,它是Googl ...

  2. Sentry-CLI 使用详解(2021 Sentry v21.8.x)

    内容源于:https://docs.sentry.io/platforms/javascript/guides/vue/ 系列 1 分钟快速使用 Docker 上手最新版 Sentry-CLI - 创 ...

  3. Geode member发现机制

    Geode member发现机制 Apache Geode 为集群和客户端服务器间提供了多种member 发现机制,具体如下: Peer Member Discovery Standalone Mem ...

  4. Python - 面向对象编程 - 实战(6)

    需求 设计一个培训机构管理系统,有总部.分校,有学员.老师.员工,实现具体如下需求: 有多个课程,课程要有定价 有多个班级,班级跟课程有关联 有多个学生,学生报名班级,交这个班级对应的课程的费用 有多 ...

  5. docker一分钟搭建nginx服务器

    运行nginx服务 拉取: docker pull nginx:1.17.9 运行: docker run -d --name nginx -P 80:80 nginx:1.17.9 -d表示在后台启 ...

  6. Openswan支持的算法及参数信息:

    数据封装加密算法: algorithm ESP encrypt: id=2, name=ESP_DES, ivlen=8, keysizemin=64, keysizemax=64 algorithm ...

  7. JS013. 重写toFixed( )方法,toFixed()原理 - 四舍五入?银行家舍入法?No!六舍七允许四舍五入√!

    以下为场景实测与原理分析,需要重写函数请直接滚动至页尾!!! 语法 - Number.prototype.toFixed( ) // toFixed()方法 使用定点表示法来格式化一个数值. numO ...

  8. k8s核心资源之Pod概念&入门使用讲解(三)

    目录 1. k8s核心资源之Pod 1.1 什么是Pod? 1.2 Pod如何管理多个容器? 1.3 Pod网络 1.4 Pod存储 1.5 Pod工作方式 1.5.1 自主式Pod 1.5.2 控制 ...

  9. MySQL高级语句(二)

    目录: 1.别名 2.子查询 3.EXISTS 4.连接查询 5.CREATE VIEW 视图 6.UNION 联集 7.交集值 8.无交集值 9.CASE 10.算排名 11.算中位数 12.算累积 ...

  10. Centos下Yum安装PHP7.0

    默认的版本太低了,手动安装有一些麻烦,想采用Yum安装的可以使用下面的方案: 1.检查当前安装的PHP包 yum list installed | grep php 如果有安装的PHP包,先删除他们 ...