要让应用优雅的停止,你应该试试ShutdownHook


“十步杀一人,千里不留行。事了拂衣去,深藏身与名”。
李白先生曾经在诗中这样描述侠客。

本篇就来说说不谈侠客拂衣去,就来看看清理现场这个操作,在Java中一般是如何做的。

对于应用,停止的时候一般都会把环境和数据恢复到运行前的样子,和单元测试时tearDown要做的效果基本类似。而为了实现停止时恢复现场,英文中有个特定的描述:

shutdown gracefully

在Java中,为了实现gracefully shutdown,有一个特定的接口可供使用,就是我们今天要提到的shutdown hook。它与回调函数功能类似,文档中对其描述如下:

关闭钩子 只是一个已初始化但尚未启动的线程。虚拟机开始启用其关闭序列时,它会以某种未指定的顺序启动所有已注册的关闭钩子,并让它们同时运行。运行完所有的钩子后,如果已启用退出终结,那么虚拟机接着会运行所有未调用的终结方法。最后,虚拟机会暂停。注意,关闭序列期间会继续运行守护线程,如果通过调用 exit 方法来发起关闭序列,那么也会继续运行非守护线程。

要为应用添加shutdownHook,需要做的只是这样的下操作:

Runtime.getRuntime().addShutdownHook(new Thread() {
    public void run() { /*
       my shutdown code here
    */ }
 });

向addShutdownHook方法传入的Thread,其run方法即为自定义的shutdown时清理逻辑。

JDK内部,是通过一个Map来保存所有添加的ShutdownHook,在被触发时执行。

public void addShutdownHook(Thread hook) {
    SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
        sm.checkPermission(new RuntimePermission("shutdownHooks"));
    }
    ApplicationShutdownHooks.add(hook);
static synchronized void add(Thread hook) {
     hooks.put(hook, hook);
}
private static IdentityHashMap<Thread, Thread> hooks;
hooks = new IdentityHashMap<>();

那这个shutdownHook一般是在什么时候会被调用呢?

我们看JDK的文档中描述,对于两类事件Java虚拟机会退出:

Java 虚拟机会为了响应以下两类事件而关闭

  • 程序正常退出,这发生在最后的非守护线程退出时,或者在调用 exit(等同于 System.exit)方法时。或者,

  • 为响应用户中断而终止 虚拟机,如键入 ^C;或发生系统事件,比如用户注销或系统关闭。 

在Java虚拟机退出的时候,这些设置的shutdownHook会被并行的调用。但需要注意的是,对于非正常方式退出Java虚拟机,例如杀进程,系统断电等,这些情况下,shutdownHook不会被执行。

我们来看,在Tomcat内部,是如何使用ShutdownHook的。

Catalina类内部,在服务器内部各个组件启动完毕后,有这样一段代码

// Register shutdown hook
if (useShutdownHook) {
    if (shutdownHook == null) {
        shutdownHook = new CatalinaShutdownHook();
    }
    Runtime.getRuntime().addShutdownHook(shutdownHook);
}

和我们在本文开头看到的一样,他注册了一个ShutdownHook。这个hook的内容如下:

/**
 * Shutdown hook which will perform a clean shutdown of Catalina if needed.
 */
protected class CatalinaShutdownHook extends Thread {
    public void run() {
        try {
            if (getServer() != null) {
                Catalina.this.stop();
            }
        } catch (Throwable ex) {
        } finally {
        }
    }
}

看就是在shutdown的时候通过调用应用服务器的stop方法,来shutdown gracefully。

而我们一般停止Tomcat会通过调用shutdown脚本的方式进行,这个时候,脚本实质上去执行了应用服务器的stop方法来进行停止,而不是被shutdownHook来反调过来的。

因此,在读源码时,你会发现代码中有这样的内容:

/**
 * Stop an existing server instance.
 */
public void stop() {

    try {
        // Remove the ShutdownHook first so that server.stop()
        // doesn't get invoked twice
        if (useShutdownHook) {
         Runtime.getRuntime().removeShutdownHook(shutdownHook);  
    } catch (Throwable t) {
        ExceptionUtils.handleThrowable(t);}

我们看到,在停止Server的时候,会先执行removeShutdownHook的操作,注释里写的明白,是为了防止server和stop被执行两次。

哪种情况下会被执行两次呢?

当使用catalina的shutdown脚本停止Server时,这个方法并不是从shutdownHook调用过来,此时,shutdownHook还没有执行,所以在JVM退出的时候,shutdownHook会被触发。如果此处还没去remove掉的话,就还会调用过来。这一点在我们自己开发应用时需要注意借鉴一下。

看到这里,希望你不要说然并卵。

当然,如果你已经脱口而出,那...

我找了例子证明,你已经在不知不觉中使用了shutdownHook,例如你在输出日志的时候,以下代码是JDK的LogManager中的一部分,我们看到,其构造方法中直接会添加一个名为Cleaner的shutdownHook。

private LogManager(Void checked) {
    // Add a shutdown hook to close the global handlers.
    try {
        Runtime.getRuntime().addShutdownHook(new Cleaner());
    } catch (IllegalStateException e) {
        // If the VM is already shutting down,
        // We do not need to register shutdownHook.
    }
}

public void run() {

    // This is to ensure the LogManager.<clinit> is completed
    // before synchronized block. Otherwise deadlocks are possible.
    LogManager mgr = manager;
    // Do a reset to close all active handlers.
    reset(); 
}

我们自己添加shutdownHook的时候,需要注意的是:

在内部不要写耗时的操作,也不要写容易引起死锁的操作。多个ShutdownHook之间如果存在资源竞争而死锁,那应用就停止不了了。

扫描二维码,即可关注!


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在Java程序中,可以通过添加关闭钩子(Shutdown Hook)来实现在程序退出时关闭资源和平滑退出的功能。关闭钩子是通过调用Runtime类的addShutdownHook(Thread hook)方法来注册的。当程序退出时,关闭钩子可以在多种场景下被调用,包括程序正常退出、使用System.exit()方法、终端使用Ctrl+C触发的中断、系统关闭以及使用Kill pid命令干掉进程等情况。\[2\] 具体到停止线程的问题,关闭钩子本身并不能直接停止线程。关闭钩子主要用于在程序退出时执行一些清理操作,例如关闭资源、保存数据等。如果需要停止线程,可以通过其他方式来实现,例如使用线程的interrupt()方法来中断线程的执行,或者使用标志位来控制线程的运行状态。关闭钩子可以在程序退出时执行这些停止线程的操作,但本身并不直接负责停止线程。\[2\] 需要注意的是,在使用关闭钩子时,应该谨慎处理资源的关闭和线程的停止,以确保程序能够正常退出并释放资源。同时,关闭钩子的执行顺序是不确定的,因此在编写关闭钩子的代码时,应该考虑到可能的并发和竞争条件,以避免出现意外的问题。\[2\]\[3\] #### 引用[.reference_title] - *1* [ShutdownHook - java中优雅停止服务](https://blog.csdn.net/zhipengfang/article/details/117451139)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* *3* [ShutdownHook Java优雅停止服务](https://blog.csdn.net/fututadeyoushang/article/details/80941632)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值