Java并发编程

JVM关闭

shutdownHook

JVM(Java Virtual Machine)的Shutdown Hook 是一种特殊的后台线程,它允许开发人员在JVM即将关闭前执行特定的清理或资源释放任务。当Java应用程序因以下情况之一需要终止时:

  • 正常退出:比如所有的非守护线程都已经终止,或者System.exit()方法被调用指示JVM退出。
  • 外部中断:操作系统发送了终止信号到Java进程,如通过kill命令或用户按下Ctrl+C。
  • 系统崩溃:虽然不常见且无法保证,但在某些情况下,即使系统遭遇严重错误导致JVM崩溃前,也可能尝试执行已注册的Shutdown Hooks。

shutdownHook注意事项

使用JVM的Shutdown Hook时,有几个重要的注意事项:

  1. 执行不确定性

    • 执行顺序不确定:JVM在关闭时会启动所有注册的Shutdown Hook线程,但这些线程之间的执行顺序是不确定的,即JVM不保证按照注册的顺序执行各个Hook。
    • 并发执行:所有Hook可能并行执行,这意味着不同Hook中的代码可能交错运行,所以Hook内部的逻辑应设计成线程安全的。
  2. 无法保证一定会执行

    • 强制退出:如果JVM收到像SIGKILL这样的立即终止信号,或者调用了Runtime.halt()方法,那么Shutdown Hook可能不会有机会被执行。
    • 系统级问题:在系统崩溃、电源故障或硬件问题等极端情况下,JVM可能来不及或无法执行任何关闭钩子。
  3. 执行时间限制

    • Shutdown Hook应该尽快完成其清理工作,因为长时间运行可能导致整个关闭过程延迟,甚至可能因为阻塞而被操作系统强制终止。
  4. 生命周期管理

    • 添加与移除:在JVM关闭序列已经开始后,不能再添加新的Shutdown Hook,也不能取消已经注册的Hook。
    • 守护进程:在JVM关闭时,守护线程并不会因为Shutdown Hook的执行而自动停止,需要在Hook中显式地处理这些线程的终止。
  5. 资源释放

    • 使用Shutdown Hook来释放诸如文件句柄、数据库连接、网络套接字等关键资源是很常见的做法,但不应依赖此机制作为唯一的数据一致性保障手段,尤其是在涉及事务性操作时,应尽量在业务逻辑层就做好资源的正确管理和异常处理。

Shutdown Hook是一种方便的工具,但它不是一种可靠的服务终止机制,对于需要高度一致性和保证执行的应用场景,应配合其他策略共同使用,如应用容器管理的生命周期回调、事务管理机制或更高级别的服务治理框架等。


为什么在Shutdown Hook的上下文中,一般希望尽可能快且干净地完成资源清理工作,以便JVM能尽快结束运行。JVM如果没有尽快结束有什么风险?

在Shutdown Hook的上下文中,之所以希望尽快且干净地完成资源清理工作,以便JVM能尽快结束运行,原因主要有以下几点:

  1. 资源占用

    • JVM进程如果迟迟未能结束,将会继续占用系统资源(如内存、CPU、文件句柄、网络连接等)。特别是当系统处于低资源环境或者是正在进行维护、重启、更新等操作时,资源的有效释放显得尤为重要。
  2. 进程僵死

    • 若JVM在关闭时卡在某个耗时很久的清理任务上,可能导致进程成为所谓的”僵死进程”,这种状态下的进程无法被正常结束,通常需要通过更强力的手段(如发送SIGKILL信号)来强制终止,而这有可能导致数据损坏或其他不可预测的问题。
  3. 系统响应

    • 在一些场景下,如服务器集群或云环境中,管理员可能希望服务在接收到关闭指令后迅速终止并释放资源,以便快速部署新的服务版本或进行系统维护。
  4. 数据完整性与一致性

    • 虽然Shutdown Hook主要用于资源清理,但如果执行时间过长,可能导致部分清理操作无法按时完成,进而影响数据的完整性和一致性。例如,若数据库连接在关闭前未能及时提交事务或回滚,可能会造成数据丢失或脏数据。
  5. 用户体验

    • 对于交互式应用而言,如果关闭过程过于缓慢,会影响用户的体验,特别是在操作系统关机或注销用户的过程中,用户可能因为应用程序未能及时关闭而被迫等待更长时间。
  6. 外部依赖与协调

    • 如果应用程序与其他系统存在紧密的集成,如分布式系统中的节点相互协作,那么一个节点的关闭速度直接影响到整个系统的协调和稳定性。

综上所述,确保JVM能在短时间内干净地关闭,不仅有利于保持系统资源的有效利用和维护操作的顺利进行,也有助于保证数据的一致性以及提升系统的整体可靠性。


若JVM在关闭时卡在某个耗时很久的清理任务上,可能导致进程成为所谓的”僵死进程”。这个时间大约多久,受什么影响

JVM在关闭时是否会成为“僵死进程”,并不在于具体耗时多久,而是看操作系统对进程终止的处理方式以及进程自身的响应。通常,操作系统会给予进程一定的时间去完成正常的关闭流程,包括执行Shutdown Hooks。这个时间并没有严格的定义,因为它受到多种因素的影响:

  1. 操作系统配置

    • 不同的操作系统有不同的默认等待策略,例如Linux系统下,针对不同的终止信号(如SIGTERM、SIGINT),系统默认等待进程自行结束的时间长度不同。
  2. JVM关闭行为

    • JVM在接收到退出请求后,会先尝试执行所有注册的Shutdown Hook,然后才会真正退出。在此期间,如果Hook中的某个任务一直阻塞,会导致JVM整体关闭时间延长。
  3. 应用程序设计

    • 应用程序如何响应关闭请求,以及Shutdown Hook中执行的任务耗时程度,直接影响到JVM关闭所需的时间。
  4. 资源状况

    • 当系统资源紧张时,操作系统可能不会给予进程很长的关闭窗口期。
  5. 人为干预

    • 用户或系统管理员可以决定是否耐心等待进程自行结束,或者采取更激进的方式如发送SIGKILL信号强制结束进程,此时进程无条件立刻终止,不再执行任何清理逻辑。

总之,“僵死进程”更多是指进程无法响应正常的终止请求,而不是指某一个确切的时间阈值。为了避免JVM在关闭时陷入僵死状态,开发人员应当确保Shutdown Hook中的清理逻辑高效、不阻塞,并在必要时采用异步或限时策略来完成清理工作。

拓展——怎么在addShutdownHook时异步关闭自定义线程池,不影响JVM关闭

如果你想在JVM关闭时触发MessageThreadPool的关闭,但又不想让这个操作阻塞JVM的关闭流程,可以采用以下方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class Application {

// 创建线程池实例
private static ExecutorService messageThreadPool = Executors.newFixedThreadPool(10);

public static void main(String[] args) {
// 应用程序的主逻辑...

// 注册JVM Shutdown Hook
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
// 异步关闭线程池
messageThreadPool.submit(() -> {
try {
messageThreadPool.shutdown(); // 先尝试平滑关闭
if (!messageThreadPool.awaitTermination(5, TimeUnit.SECONDS)) { // 设置超时时间
messageThreadPool.shutdownNow(); // 如果超时,则强制关闭
}
} catch (InterruptedException e) {
// 当中断发生时,转换为强制关闭
messageThreadPool.shutdownNow();
Thread.currentThread().interrupt();
}
});

// 其他可能的资源清理操作...
}));
}
}

上述代码示例中,我们创建了一个固定大小的线程池MessageThreadPool,并在JVM的Shutdown Hook中对其进行异步关闭。通过submit方法提交一个任务到线程池,该任务负责调用shutdown方法并设置一个超时时间,如果在指定时间内线程池内的任务仍未全部完成,则调用shutdownNow进行强制关闭。

这样一来,即便关闭线程池的过程耗时较长,也不会阻塞JVM的关闭流程,因为这部分操作是在一个新的线程中异步执行的。同时,设置了超时时间,确保在一定程度上约束了关闭操作的执行时间,提高了JVM关闭的确定性。

rocketmq/docs/en/Example_OpenMessaging.md at 59220d8f582a329c5f9e775c371f57ec1ff3ff39 · apache/rocketmq (github.com)

ice/java/README.md at 6c755c89f54d86372e2c2537de6d09fe4323967e · zeroc-ice/ice (github.com)

starrocks/fe/fe-core/src/main/java/com/starrocks/StarRocksFE.java at 29369cc3ddb290d5e90f0a67974a58656168a321 · StarRocks/starrocks (github.com)



----------- 本文结束 -----------




0%