linux 后台线程优雅退出,JVM优雅退出

背景

在某个Java应用增加新功能,缩容机器,或者应用以及机器发生异常,通常会停止正在运行的应用,该应用通常正在运行着任务,如果停止应用的操作处理不当的话,很有可能会导致数据丢失,损坏,从而影响业务。所以在停止应用的时候,需要考虑如何安全优雅的退出。本文分成三部分:

jvm关闭的几种情况

如何优雅关闭应用

几点注意事项

jvm关闭的几种情况

jvm通常有下面几种关闭的情况:

正常关闭

1. 所有非daemon线程退出

2. 调用System.exit()

3. SIGINT(ctrl+c)

4. SIGTERM(kill -15)

异常关闭

1. 未捕获的异常

2. oom

强制关闭

1. SIGKILL(kill -9)

2. 应用crash

3. 机器宕机

对于正常关闭、异常关闭的几种情况,JVM关闭前,都会调用已注册的shutdown hooks。

对于强制关闭的几种情况,会直接停止JVM进程,JVM不会调用已注册的shutdown hooks。

其中有几点需要了解:

linux信号以及处理

其中ctrl+c,kill -15,kill -9 都是通过发送信号的方式来通知jvm进行关闭操作。

daemon线程和非daemon线程区别

daemon线程在jvm里面的定义是: 如果jvm中只有daemon线程在运行,则jvm退出。通常默认新起的线程都是非daemon线程,可以通过设置线程的属性,将线程设置为daemon线程。

jvm shutdown hooks

jvm提供了Runtime.getRuntime().addShutdownHook方法用来注册自定义的关闭逻辑,如下所示是spring框架注册的关闭逻辑

public void registerShutdownHook() {

if (this.shutdownHook == null) {

// No shutdown hook registered yet.

this.shutdownHook = new Thread() {

@Override

public void run() {

synchronized (startupShutdownMonitor) {

doClose();

}

}

};

Runtime.getRuntime().addShutdownHook(this.shutdownHook);

}

}

关闭钩子本质上是一个线程,对于一个JVM中注册的多个关闭钩子会并发执行,所以JVM并不保证它们的执行顺序,建议在一个钩子中执行应用的关闭操作。

在关闭钩子中,不能执行注册、移除钩子的操作,JVM将关闭钩子序列初始化完毕后,不允许再次添加或者移除已经存在的钩子,否则JVM抛出 IllegalStateException。不能在钩子调用System.exit(),否则卡住JVM的关闭过程,但是可以调用Runtime.halt()。

jvm退出时会等待所有的钩子线程执行之后,再退出jvm,到时候会直接停止运行还在运行所有的线程(包括daemon和非daemon)。

如何实现优雅关闭

通常一个应用会有两种场景:

外部驱动:接收外部的请求,转换成任务,并运行该任务。

内部驱动:自产自销,通常以一个定时任务的形式运行。

针对第一种场景,需要从两个方面考虑优雅关闭:

停止接收外部新的请求。

对于rpc调用,需要在注册中心主动注销自身的服务,从而避免上游应用继续往该机器发送请求;对于MQ消费,需要主动告知MQ停止往该机器投递消息。

等待当前接收的所有任务执行完成

通常会通过线程池的方式来实现,可以直接调用线程池的shutdown方法,注意需要了解shutdownNow和shutdown以及awaitTermination方法的使用

针对第二种场景,定时任务通常有两种实现方式:

使用ScheduldExecutorService线程池方式,优雅退出直接参考上面的线程池关闭即可。

在单线程在里面while循环加sleep的方式实现,通常有两种优雅退出方式:

使用volatile类型的共享变量作为退出标志

通常在使用一个变量标识作为while循环判断的依据,该变量启动之后值为true,在自定义的jvm关闭钩子里面,修改该变量的值为false,为false时停止退出循环,结束线程。

通过Thread.interrupt方法

在jvm关闭钩子里面,获取到需要关闭的线程对象,调用thread.interrupt方法,如果此线程处于阻塞状态(比如调用了sleep方法,wait方法,或者io等待),则会立马退出阻塞,并抛出InterruptedException异常;如果此线程正处于运行之中,则线程不受任何影响,继续运行,仅仅是置线程的状态为中断状态interrupted。

在编写线程相关的代码时,需要捕获InterruptedException异常(捕获到异常时会清除interrupted状态),并且在合理的位置调用 isInterrupted方法来判断查看自己是否被中断,并做退出操作。

几点注意事项

现在一般的应用都是基于spring框架开发的,spring框架本身会注册shutdown hook,关闭的时候会根据bean的依赖关系按序执行bean的destory逻辑,先destory被依赖的bean。呈一个树状的destory顺序。在编写bean实例相关代码的时候,最好加上destory方法,并做退出操作。

注册jvm钩子的时候,最好设置线程名,并打印关键日志,以便排除问题。

在所有的shutdown hook执行结束之后,会直接停止jvm,这一动作会停止还在运行的所有的线程(包括daemon和非daemon),所以最好在hook的代码里面增加需要关闭线程的状态判断,保证线程先关闭,再结束hook的运行。

因为jvm钩子是异步并发执行的,可能会乱序,所以可以强制指定jvm只执行自己设置的钩子,去除其他引入的JAR包里面注册的钩子。jvm钩子的源码如下:

public class Runtime {

public void addShutdownHook(Thread hook) {

SecurityManager sm = System.getSecurityManager();

if (sm != null) {

sm.checkPermission(new RuntimePermission("shutdownHooks"));

}

ApplicationShutdownHooks.add(hook);

}

...

}

class ApplicationShutdownHooks {

/* The set of registered hooks */

private static IdentityHashMap hooks;

static synchronized void add(Thread hook) {

hooks.put(hook, hook);

}

/* Remove a previously-registered hook. Like the add method, this method

* does not do any security checks.

*/

static synchronized boolean remove(Thread hook) {

return hooks.remove(hook) != null;

}

/* Iterates over all application hooks creating a new thread for each

* to run in. Hooks are run concurrently and this method waits for

* them to finish.

*/

static void runHooks() {

Collection threads;

synchronized(ApplicationShutdownHooks.class) {

threads = hooks.keySet();

hooks = null;

}

for (Thread hook : threads) {

hook.start();

}

for (Thread hook : threads) {

while (true) {

try {

hook.join();

break;

} catch (InterruptedException ignored) {

}

}

}

}

Runtime.getRuntime().addShutdownHook本身会调用ApplicationShutdownHooks注册钩子,hooks维护了所有已经注册的钩子,由于jvm本身没有提供好用的方法去移除已经注册的钩子,可以通过反射的方式调用ApplicationShutdownHooks的hooks属性并清除该map里面的内容。

需要考虑接受到关闭信号之后,jvm长时间没有关闭的情况,避免应用关闭失败。通常设置最大等待时间,最大等待时间之后,直接强制关闭,比如执行kill -9。

e8fba41fa501

梵高的田野

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值