Java并发编程实战 08 | 彻底理解Shutdown Hook

钩子线程(Hook Thread)简介

在一个 Java 应用程序即将退出时(比如通过正常执行完成或通过用户关闭应用程序),通常需要进行一些清理操作,例如:

  • 释放资源(如文件句柄、网络连接)。
  • 关闭数据库连接。
  • 保存未完成的数据或状态。

我们可以通过钩子线程实现这一点,钩子线程是指在程序结束时,JVM 会自动执行的一类线程。这些线程会被预先“挂钩”在程序退出事件上,一旦 JVM 检测到程序即将退出,就会启动这些线程来执行特定的操作。

钩子线程是通过 Runtime.getRuntime().addShutdownHook(Thread hook) 方法来注册的。当 JVM 检测到应用程序即将退出时,就会运行所有注册的钩子线程。

来看一个示例代码:

public class HookThreadDemo {

    private static class HookRunnable implements Runnable {
        @Override
        public void run() {
            try {
                System.out.println("Hook " + Thread.currentThread().getName() + " is executing...");
                Thread.sleep(2000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("Hook " + Thread.currentThread().getName() + " is about to end execution");
        }
    }

    public static void main(String[] args) {
        HookRunnable hookRunnable = new HookRunnable();
        //add hook thread 0
        Runtime.getRuntime().addShutdownHook(new Thread(hookRunnable));
        //add hook thread 1
        Runtime.getRuntime().addShutdownHook(new Thread(hookRunnable));

        System.out.println("The main thread is going to finish executing.");
    }
}

//输出:
The main thread is going to finish executing.is going to finish executing.
Hook Thread-0 is executing...
Hook Thread-1 is executing...
Hook Thread-1 is about to end execution
Hook Thread-0 is about to end execution

从输出中可以看到,当主线程执行完毕,也就是JVM进程即将退出的时候,两个注入的Hook线程都会被启动,并且打印出相关日志。

Shutdown Hook 机制的应用场景

利用 Shutdown Hook 机制可以完成一些在程序退出前的清理和后续处理工作,例如:

  1. 释放资源:在 Hook 中释放文件句柄、数据库连接等资源,避免资源泄漏。
  2. 关闭服务:在 Hook 中关闭服务器,确保所有请求都已处理完毕,安全地终止服务。
  3. 发送通知:在 Hook 中发送电子邮件、短信等通知,告知用户或管理员服务已停止。
  4. 记录日志:在 Hook 中记录系统状态、错误信息等日志,便于事后排查和分析问题。

数据库连接关闭案例

下面简单演示一下如何使用Shutdown Hook机制关闭数据库连接。

public class DataBaseConnectMain {
    private static Connection conn;

    public static void main(String[] args) {

        System.out.println("The main thread starts executing");

        // 初始化数据库连接
        initConnection();

        System.out.println("Do some data querying and processing");

        // 注册关闭钩子
        Runtime.getRuntime().addShutdownHook(new Thread() {
            public void run() {
                closeConnection();
            }
        });

        System.out.println("The main thread ends execution.");
    }

    private static void initConnection() {
        try {
            Class.forName("com.mysql.cj.jdbc.Driver");
            conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/school_info?useSSL=true&", "root", "root");
            System.out.println("Database connection successful!");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    private static void closeConnection() {
        try {
            conn.close();
            System.out.println("Database connection closed!");
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}


//输出:
The main thread starts executing
Database connection successful!
Do some data querying and processing
The main thread ends execution.
Database connection closed!

上述代码中我们在initConnection()方法中初始化了一个数据库连接,同时在main()函数中注册了一个 Shutdown Hook,用于在 JVM 关闭时关闭数据库连接。从输出可以看出,进程关闭时,输出"Database connection closed!"

Shutdown Hook 机制使用注意事项

  1. Hook线程只有在正确接收到退出信号的情况下才能正常执行。如果你通过强制方法(例如 kill -9)杀死进程,Hook 线程将不会被执行,因为它们无法应对这种情况。
  2. 不要在 Hook 线程中执行会导致程序长时间无法退出的耗时操作。
  3. 尽量避免在 Hook 线程中抛出异常,否则可能导致 Java 虚拟机无法正常退出。
  4. Shutdown Hooks 的注册顺序非常重要,需要根据它们之间的依赖关系进行合理安排。通常应先注册简单的 Shutdown Hooks,再注册复杂的。
  5. 尽量不要在 Shutdown Hook 中启动新线程,否则可能导致 JVM 无法正常关闭。

Shutdown Hook机制在开源框架中的使用

1. Spring

2.Tomcat

Shutdown Hook 机制的原理

Java 的 Shutdown Hook 机制依赖于 Java 虚拟机(JVM)中的两个线程:主线程和Shutdown 线程。

当 Java 应用程序启动时,主线程会创建一个 Shutdown 线程,并将所有注册的 Shutdown Hooks 添加到 Shutdown 线程的 Hook 列表中。当 JVM 收到终止信号时,它会首先停止所有用户线程,然后启动 Shutdown 线程。

Shutdown 线程会按照 Hook 列表中的顺序逐一执行每一个 Hook,并等待所有 Hook 执行完毕或超时。如果所有 Hook 都执行完毕,JVM 将正常退出;否则,JVM 将强制退出。

Shutdown Hook 机制源码分析

根据Hook机制的原理介绍,对源码的分析我们主要从3个方面入手:

  1. 如何注册一个ShutdownHook线程;
  2. 如何执行 ShutdownHook 线程。
  3. 当 ShutdownHook 被触发时;
1. ShutdownHook的注册

当我们添加一个 ShutdownHook 时,ApplicationShutdownHooks.add(hook)将被调用;

传入的钩子线程会被添加到 ApplicationShutdownHooks 类的静态变量 private static IdentityHashMap<Thread, Thread> hooks 中,这个变量维护着所有后续需要使用的钩子。

在 ApplicationShutdownHooks 类初始化时,其 hooks 会被添加到 Shutdown 的 hooks 中,并且执行顺序固定为第一位。

Shutdown 类中的 hooks 是系统级的 ShutdownHooks,系统级的 ShutdownHooks 由一个数组组成,最多只能添加 10 个。在这种情况下,我们只需要关注顺序为 1 的钩子,也就是 ApplicationShutdownHooks。

2. ShutdownHook 的执行

Shutdown 类通过调用 runHooks 方法来运行之前注册的系统级 ShutdownHooks。它直接调用线程类的 run 方法(而不是从 start 方法开始)。结合源码可以知道,每个系统级 ShutdownHook 都是同步、有序地执行的。

当系统级钩子运行到序号为 1 的钩子时,ApplicationShutdownHooks 的 runHooks 方法会被执行。

在方法内部,每个钩子在执行时会调用线程类的 start 方法,,所以应用程序级别的Shutdown Hook是异步执行的,但在退出之前会等待所有钩子执行完毕。

3. Shutdown Hook的触发时刻

跟踪 Shutdown 的 runHooks 线程,我们得出了以下调用路径。

重点关注 Shutdown.exit 和 Shutdown.shutdown 的调用。

Shutdown.exit

我们发现 Shutdown.exit 的调用者包括 Runtime.exit 和 Terminator.setup。

  • Runtime.exit 是代码中用于主动结束程序的接口。
  • Terminator.setup 在 initializeSystemClass 中被调用,在第一个线程初始化时触发。它注册了一个信号监听函数,用于捕获 kill 信号,并通过调用 Shutdown.exit 来结束进程。

这些涵盖了代码中的终止场景,包括进程主动终止和进程被 kill 命令杀死。主动结束进程的过程较为直观,因此这里重点讲解如何实现信号捕获。可以通过在终端输入 kill -l 来查看系统支持的信号。

下面我们简单介绍一下一些常用的信号及含义:

Signal Name             Serial No.        Meaning
HUP                     1               Terminal disconnected1               Terminal disconnected
INT                     2               Interrupt (same as Ctrl + C)
QUIT                    3               Exit (same as Ctrl + \)
TERM                    15              Normal termination
KILL                    9               Forced termination
CONT                    18              Continue (opposite of STOP, fg/bg command)
STOP                    19              Stop(same as Ctrl + Z)
USR1                    10              User defined signal 1
USR2                    12              User defined signal 2

在 Java 中,我们可以通过编写以下代码来捕获 kill 信号。只需实现 SignalHandler 接口并重写 handle 方法,然后在程序入口处注册相应的信号进行监听即可。

不过,需要注意,并不是所有信号都可以被捕获和处理。

public class SignalHandlerTest implements SignalHandler {

    public static void main(String[] args) {

        Runtime.getRuntime().addShutdownHook(new Thread(() -> System.out.println("ShutdownHook is running...")));

        SignalHandler sh = new SignalHandlerTest();
        Signal.handle(new Signal("INT"), sh);
        Signal.handle(new Signal("TERM"), sh);

        //Signal.handle(new Signal("QUIT"), sh);//  This signal cannot be captured
        //Signal.handle(new Signal("KILL"), sh);//  This signal cannot be captured

        while (true) {
            System.out.println("Main thread is 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);
    }
}

将上面的代码打成JAR包,然后在命令行中执行,看下图,主要分为五个部分:

  1. 运行JAR包,启动进程;
  2. 主线程正在执行,并监听信号;
  3. 用户输入信号ctrl + c,即INT-2
  4. 收到信号后,输出信号类型,流程结束;
  5. 在结束进程之前,执行ShowdonwHook的逻辑。

需要注意的是,一般来说,当我们捕获到信号后,完成个性化处理后,需要主动调用 System.exit,否则进程将不会退出,只能通过 kill -9 强制杀死进程。

此外,由于各个信号的捕获是在不同的线程中进行的,因此它们的执行是异步的。

Shutdown.shutdown

该方法的调用时机可以从代码注释中找到:

在 Java 中,线程分为两种类型:用户线程和守护线程。守护线程是服务于用户线程的,例如垃圾回收线程(GC)。JVM 判断是否可以结束的标志是是否还有用户线程在运行。当最后一个用户线程结束时,Shutdown.shutdown 会被调用。这是 JVM 和虚拟机语言特有的“特权”。关于守护线程的更多细节将在后面的文章中介绍。

因此,通过对 Shutdown.exit 和 Shutdown.shutdown 的分析,我们可以总结出以下结论:

其实,Java 的 ShutdownHook 已经覆盖了大部分终止场景,但有一个情况无法处理,那就是当我们使用 kill -9 强制杀死进程时,由于程序无法捕获和处理这种强制终止信号,进程会被直接杀死,因此 ShutdownHook 无法顺利执行。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/891016.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

解锁C++继承的奥秘:从基础到精妙实践(下)

文章目录 前言&#x1f950;五、多继承&#xff0c;菱形继承和菱形虚拟继承&#x1f9c0;5.1 多继承&#x1f9c0;5.2 菱形继承&#x1f9c0;5.3 虚拟继承&#xff08;解决菱形继承问题&#xff09;5.3.1 虚拟继承的语法&#xff1a;5.3.2 虚拟继承示例&#xff1a; &#x1f9…

大舍传媒-海外媒体发稿:为您打造全球品牌影响力

大舍传媒-海外媒体发稿&#xff1a;为您打造全球品牌影响力 在当今全球化的商业环境中&#xff0c;企业若想在激烈的市场竞争中脱颖而出&#xff0c;拓展全球市场&#xff0c;提升品牌影响力至关重要。大舍传媒的海外媒体发稿服务&#xff0c;正是您实现这一目标的得力助手。 …

面对服务器掉包的时刻困扰,如何更好的解决

在数字化时代&#xff0c;服务器的稳定运行是企业业务连续性的基石。然而&#xff0c;服务器“掉包”现象&#xff0c;即数据包在传输过程中丢失或未能正确到达目的地的情况&#xff0c;却时常成为IT运维人员头疼的问题。它不仅影响用户体验&#xff0c;还可能导致数据不一致、…

【AI 新观察】“转人工!转人工!”——智能客服痛点与破局之路

在当今数字化时代&#xff0c;智能客服在电商等众多领域被广泛应用&#xff0c;然而&#xff0c;一句又一句“转人工&#xff01;转人工&#xff01;”却常常暴露出智能客服存在的痛点。一、智能客服之痛 1. 理解偏差引不满 智能客服在理解客户问题时&#xff0c;常常出现偏差…

mybatisPlus对于pgSQL中UUID和UUID[]类型的交互

在PGSQL中&#xff0c;有的类型是UUID和UUID[]这种类型&#xff0c;在mybatis和这些类型交互的时候需要手动设置类型处理器才可以&#xff0c;这里记录一下类型处理器的设置 /*** UUID类型处理器*/ public class UUIDTypeHandler extends BaseTypeHandler<UUID> {/*** 获…

Golang | Leetcode Golang题解之第478题在圆内随机生成点

题目&#xff1a; 题解&#xff1a; type Solution struct {radius, xCenter, yCenter float64 }func Constructor(radius, xCenter, yCenter float64) Solution {return Solution{radius, xCenter, yCenter} }func (s *Solution) RandPoint() []float64 {r : math.Sqrt(rand.…

热更新解决方案2 —— Lua语法相关知识点

概述 开发环境搭建 Lua语法 1.第一个Lua程序 2.变量 print("******变量*******"); --lua当中的简单变量类型 -- nil number string boolean -- lua 中所有的变量声明 都不需要声明变量类型 它会自动的判断类型 -- 类似C# 中的var --lua中的一个变量 可以随便赋值 ——…

Product1M 深度理解 PPT

系列论文研读目录 文章目录 系列论文研读目录 模态内检索&#xff1a;是指在同一模态&#xff08;例如&#xff0c;图像、文本或音频&#xff09;中进行的检索任务。它通常涉及在同一类型的数据中查找相关项。比如下面图像只能查询图像&#xff0c;文本只能查询文本&#xff0c…

modbus tcp wireshark抓包

Modbus TCP报文详解与wireshark抓包分析_mbap-CSDN博客 关于wireshark无法分析出modbusTCP报文的事情_wireshark 协议一列怎么没有modbus tcp-CSDN博客 使用Wireshark过滤Modbus功能码 - 技象科技 连接建立以后才能显示Modbus TCP报文 modbus.func_code 未建立连接时&…

D36【python 接口自动化学习】- python基础之函数

day36 函数的定义 学习日期&#xff1a;20241013 学习目标&#xff1a;输入输出与文件操作&#xfe63;-49 函数定义&#xff1a;如何优雅地反复引用同一段代码&#xff1f; 学习笔记&#xff1a; 函数的用途 定义函数 调用函数 # 定义函数 def foo():print(foo)print(foo …

胤娲科技:AI短视频——创意无界,即梦启航

在这个快节奏的时代&#xff0c;你是否曾梦想过用几秒钟的短视频&#xff0c;捕捉生活中的每一个精彩瞬间&#xff1f;是否曾幻想过&#xff0c;即使没有专业的摄影和剪辑技能&#xff0c;也能创作出令人惊艳的作品&#xff1f; 现在&#xff0c;这一切都不再是遥不可及的梦想。…

一区鱼鹰优化算法+深度学习+注意力机制!OOA-TCN-LSTM-Attention多变量时间序列预测

一区鱼鹰优化算法深度学习注意力机制&#xff01;OOA-TCN-LSTM-Attention多变量时间序列预测 目录 一区鱼鹰优化算法深度学习注意力机制&#xff01;OOA-TCN-LSTM-Attention多变量时间序列预测预测效果基本介绍程序设计参考资料 预测效果 基本介绍 1.基于OOA-TCN-LSTM-Attenti…

Mysql(八) --- 视图

文章目录 前言1.什么是视图&#xff1f;2.创建视图3. 使用视图4. 修改数据4.1.注意事项 5. 删除视图6.视图的优点 前言 前面我们学习了索引&#xff0c;这次我们来学习视图 1.什么是视图&#xff1f; 视图是一个虚拟的表&#xff0c;它是基于一个或多个基本表或其他视图的查询…

Docker 入门篇

&#x1f3dd;️ 博主介绍 大家好&#xff0c;我是一个搬砖的农民工&#xff0c;很高兴认识大家 &#x1f60a; ~ &#x1f468;‍&#x1f393; 个人介绍&#xff1a;本人是一名后端Java开发工程师&#xff0c;坐标北京 ~ &#x1f389; 感谢关注 &#x1f4d6; 一起学习 &…

05 django管理系统 - 部门管理 - 修改部门

04我们已经实现了新增部门的功能&#xff0c;下面开始修改部门模块的实现。 按道理来说&#xff0c;应该是做成弹框样式的&#xff0c;通过ajax悄咪咪的发数据&#xff0c;然后更新前端数据&#xff0c;但是考虑到实际情况&#xff0c;先用页面跳转的方式实现&#xff0c;后面…

106页PPT企业管控模式方案:战略、产业与职能管理体系核心规划

企业集团管控模式的设计方案是一个复杂而系统的过程&#xff0c;其核心规划涉及到战略、产业与职能管理体系。以下是对这三个方面的详细规划&#xff1a; 一、战略规划 明确集团战略目标&#xff1a;集团应根据市场环境和自身优势&#xff0c;明确战略发展方向和目标&#xf…

Tailwind Starter Kit 一款极简的前端快速启动模板

Tailwind Starter Kit 是基于TailwindCSS实现的一款开源的、使用简单的极简模板扩展。会用Tailwincss就可以快速入手使用。Tailwind Starter Kit 是免费开源的。它不会在原始的TailwindCSS框架中更改或添加任何CSS。它具有多个HTML元素&#xff0c;并附带了ReactJS、Vue和Angul…

JavaScript 网页设计案例:使用 Canvas 实现趣味打气球小游戏

JavaScript 网页设计案例&#xff1a;使用 Canvas 实现趣味打气球小游戏 在网页设计中&#xff0c;交互性和趣味性是吸引用户的重要因素。借助 JavaScript 和 HTML5 的 canvas 元素&#xff0c;我们可以轻松实现各种动画效果&#xff0c;今天将带你打造一个有趣的 打气球小游戏…

Metasploit渗透测试之攻击终端设备和绕过安全软件

概述 在之前&#xff0c;重点讨论了针对服务器端的利用。但在当下&#xff0c;最成功的攻击都是针对终端的&#xff1b;原因是&#xff0c;随着大部分安全预算和关注都转向面向互联网的服务器和服务&#xff0c;越来越难找到可利用的服务&#xff0c;或者至少是那些还没有被破…

大规模多传感器滑坡检测数据集,利用landsat,哨兵2,planet,无人机图像等多种传感器采集数据共2w余副图像,mask准确标注滑坡位置

大规模多传感器滑坡检测数据集&#xff0c;利用landsat&#xff0c;哨兵2&#xff0c;planet&#xff0c;无人机图像等多种传感器采集数据共2w余副图像&#xff0c;mask准确标注滑坡位置 大规模多传感器滑坡检测数据集介绍 数据集概述 名称&#xff1a;大规模多传感器滑坡检测…