首页 > 编程语言 >Java并发编程实战 08 | 彻底理解Shutdown Hook

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

时间:2024-09-08 19:53:51浏览次数:6  
标签:Java JVM Thread 08 System Hook 线程 Shutdown

钩子线程(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 无法顺利执行。

标签:Java,JVM,Thread,08,System,Hook,线程,Shutdown
From: https://blog.csdn.net/weixin_42627385/article/details/142031592

相关文章

  • 【JavaScript】LeetCode:16-20
    文章目录16无重复字符的最长字串17找到字符串中所有字母异位词18和为K的子数组19滑动窗口最大值20最小覆盖字串16无重复字符的最长字串滑动窗口+哈希表这里用哈希集合Set()实现。左指针i,右指针j,从头遍历数组,若j指针指向的元素不在set中,则加入该元素,否则更新......
  • JAVA代理-----详细深入介绍
    什么是代理(定义)定义:给目标对象提供一个代理对象,并且由代理对象控制对目标对象的引用为什么要使用JAVA代理(目的)1.功能增强:通过代理业务对原有业务进行增强2.控制访问通过代理对象的方式间接的范文目标对象,防止直接访问目标对象给系统带来不必要的复杂性。例:银行转账的系统......
  • 【Java】Word题库解析2
     初稿见:https://www.cnblogs.com/mindzone/p/18362194一、新增需求在原稿题库之后,还需要生成一份纯题目+ 纯答案答案放在开头,题目里面去掉答案在检查题型时还发现部分内容略有区别: 所以在判断是否为答案的时候需要兼容这种答案二、关于老版本支持doc2000版需要追加......
  • 08 Midjourney从零到商用·基础篇:多重提示词语义分割&权重
    今天,我想更深入地研究一下多重提示这个功能。它允许在单个图像中描述不同的概念,并为这些概念分配不同的重要性级别。让我们详细了解一下这个功能的机制,探索它的工作原理,并提供更多示例来展示它的多功能性。语义分割用::来表示,增加元素权重和语义分割以太空和船为例“sp......
  • 1-4Java修饰符
    Java修饰符Java语言提供了很多修饰符,主要分为以下两类:访问修饰符非访问修饰符修饰符用来定义类,方法或者变量,通常放在语句的最前端。访问控制修饰符Java中,可以使用访问控制符来保护对类,方法,变量,构造方法的访问。Java支持4种不同的访问权限。default(即默认,什么也不写):在......
  • Java基础第六天-面向对象编程
    类与对象类就是数据类型,对象就是一个具体的实例。类拥有属性和行为。类是抽象的,概念的,代表一类事物,比如人类,猫类等它是数据类型。对象是具体的,实际的,代表一个具体事物,即是实例。类是对象的模板,对象是类得一个个体,对应一个实例。对象在内存中的存在形式:字符串是指向地址保......
  • Linux和C语言(Day08)
    一、周练习1.题目一:(25分)1.题目描述:输入终值,输出所有能被7整除的数值及其和              2.评分要求根据接收值准确定义变量类型(2分)提示并输入终值(2分)阅读题目确定循环要素:起始值、终值、步长(3分)循环判断指定范围内能被7整除的数值并输出(5分)核......
  • 6.跟着狂神学JAVA(数组)
    数组数组是相同类型数据的有序集合每一个数据称作一个数据元素,每个数组元素可以通过一个下标来访问获取数组长度:array.length数组的使用声明数组dataType[]arrayName;初始化数组在声明时初始化int[]numbers=newint[5];//创建一个长度为5的整型数组在声明......
  • 7.跟着狂神学JAVA(面向对象)
    什么是面向对象面向过程步骤清晰简单适合处理一些较为简单的问题线性思维面向对象先分类、然后对分类后的细节进行面向过程的思考适合处理复杂、多人协作的问题分类思维面向对象编程的本质是:以类的方式组织代码,以对象的组织(封装)数据抽象从认识论的角度考......
  • Java毕业设计源码 - ssm框架网上服装销售系统+jsp+vue+数据库mysql+毕业论文等
    文章目录前言一、毕设成果演示(源代码在文末)二、毕设摘要展示1、开发说明2、需求/流程分析3、系统功能结构三、系统实现展示1、用户功能模块2、管理员功能模块四、毕设内容和源代码获取总结逃逸的卡路里博主介绍:✌️码农一枚|毕设布道师,专注于大学生项目实战开发、......