关闭钩子简介
当程序即将退出时(例如释放资源、关闭数据库连接等),可以通过预先注册一个或多个关闭钩子线程(Shutdown Hook)来执行相关操作。当 JVM 进程准备退出时,这些钩子线程会被触发并运行。
示例代码:
public class HookThreadDemo {
privatestaticclass HookRunnable implements Runnable {
@Override
public void run() {
try {
System.out.println("钩子线程 " + Thread.currentThread().getName() + " 正在执行...");
Thread.sleep(2000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("钩子线程 " + Thread.currentThread().getName() + " 执行结束");
}
}
public static void main(String[] args) {
HookRunnable hookRunnable = new HookRunnable();
// 添加钩子线程 0
Runtime.getRuntime().addShutdownHook(new Thread(hookRunnable));
// 添加钩子线程 1
Runtime.getRuntime().addShutdownHook(new Thread(hookRunnable));
System.out.println("主线程即将结束执行");
}
}
输出结果:
主线程即将结束执行
钩子线程 Thread-0 正在执行...
钩子线程 Thread-1 正在执行...
钩子线程 Thread-1 执行结束
钩子线程 Thread-0 执行结束
当主线程执行完毕后,JVM 进程退出前,所有注册的钩子线程会被启动并执行。
关闭钩子应用场景
-
释放资源:关闭文件句柄、数据库连接等,避免资源泄漏。 -
停止服务:安全关闭服务器,确保所有请求处理完毕。 -
发送通知:通过邮件或短信通知用户服务已停止。 -
记录日志:保存系统状态或错误信息,便于后续排查问题。
数据库连接实战演示
以下代码演示如何用关闭钩子关闭数据库连接:
public class DatabaseConnection {
privatestatic Connection conn;
public static void main(String[] args) {
System.out.println("主线程开始执行");
initConnection(); // 初始化数据库连接
System.out.println("执行数据查询与处理");
// 注册关闭钩子
Runtime.getRuntime().addShutdownHook(new Thread(() -> closeConnection()));
System.out.println("主线程结束执行");
}
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("数据库连接成功!");
} catch (ClassNotFoundException | SQLException e) {
e.printStackTrace();
}
}
private static void closeConnection() {
try {
conn.close();
System.out.println("数据库连接已关闭!");
} catch (SQLException e) {
e.printStackTrace();
}
}
}
输出结果:
主线程开始执行
数据库连接成功!
执行数据查询与处理
主线程结束执行
数据库连接已关闭!
使用关闭钩子的注意事项
-
强制终止进程(如 kill -9
)不会触发钩子线程。 -
避免耗时操作:钩子线程中不要执行长时间任务,否则会延迟 JVM 退出。 -
禁止异常抛出:钩子线程中的异常可能导致 JVM 无法正常退出。 -
注册顺序:按依赖关系注册钩子,先注册简单任务,后注册复杂任务。 -
避免启动新线程:在钩子中启动新线程可能导致 JVM 无法正常关闭。
开源框架中的关闭钩子机制
1. Spring
在AbstractApplicationContext
中,registerShutdownHook()
方法注册钩子,用于关闭上下文:
public void registerShutdownHook() {
if (this.shutdownHook == null) {
this.shutdownHook = new Thread(() -> doClose());
Runtime.getRuntime().addShutdownHook(this.shutdownHook);
}
}
2. Tomcat
Tomcat 通过注册钩子确保服务关闭时释放资源:
public void registerShutdownHook() {
if (this.shutdownHook == null) {
this.shutdownHook = new Thread(() -> {
synchronized (startupShutdownMonitor) {
doClose();
}
});
Runtime.getRuntime().addShutdownHook(this.shutdownHook);
}
}
关闭钩子机制的原理
JVM 启动时,主线程会创建一个关闭线程(Shutdown Thread),并将所有注册的钩子添加到其任务列表中。当 JVM 收到终止信号时:
-
停止所有用户线程。 -
启动关闭线程,按顺序执行钩子任务。 -
等待所有钩子执行完毕或超时后退出。
钩子的注册与执行
-
注册:通过 Runtime.getRuntime().addShutdownHook(Thread)
将线程添加到ApplicationShutdownHooks
的静态列表中。 -
执行:关闭线程按顺序同步执行系统级钩子,异步执行应用级钩子,并等待所有线程完成。
关闭钩子的触发时机
-
主动调用:通过 Runtime.exit()
或System.exit()
触发。 -
信号捕获:JVM 注册信号处理器(如 INT
、TERM
),捕获kill
命令发送的信号后触发。
示例代码(捕获信号):
public class SignalHandlerTest implements SignalHandler {
public static void main(String[] args) {
Runtime.getRuntime().addShutdownHook(new Thread(() ->
System.out.println("关闭钩子正在运行...")));
SignalHandler handler = new SignalHandlerTest();
Signal.handle(new Signal("INT"), handler); // 捕获 Ctrl+C
Signal.handle(new Signal("TERM"), handler); // 捕获 kill 命令
while (true) {
System.out.println("主线程运行中...");
Thread.sleep(2000);
}
}
@Override
public void handle(Signal signal) {
System.out.println("接收到信号:" + signal.getName() + "-" + signal.getNumber());
System.exit(0);
}
}
输出示例:
主线程运行中...
主线程运行中...
^C接收到信号:INT-2
关闭钩子正在运行...
信号处理与守护线程
-
信号不可捕获的情况: KILL
(9)和QUIT
(3)无法被捕获。 -
守护线程:JVM 在所有用户线程结束后自动退出,守护线程(如 GC 线程)不会阻止 JVM 退出。
总结
Java 的关闭钩子机制覆盖了大部分退出场景,但以下情况例外:
-
使用 kill -9
强制终止进程时,钩子不会执行。 -
信号处理需调用 System.exit()
确保进程退出。
通过合理使用关闭钩子,可以实现资源释放、服务优雅关闭等关键功能。
原文始发于微信公众号(程序猿技术充电站):Java并发编程:优雅的关闭钩子(Shutdown Hook)
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/312280.html