Java 中的优雅停机
停机的时候尽量让正在运行的任务不报错的退出就是所谓的优雅停机了,一般的做法就是采用停机事件监听机制。 这里介绍下 java 应用中的优雅停机。
JDK 中的 Runtime.addShutdownHook
JDK 自带的 Hook 处理,接收一个 Thread 对象。 JVM 收到退出请求时会挨个激活这些线程。 可以用来做一些退出的清理工作,例如 Dubbo 的优雅停机 就是通过这种方式实现的. 使用时需要注意
- 不保证 Hook 一定会被执行,例如
kill -9
- 没有顺序保证,Hook 的 Thread 对象会作为 IdentityHashMap 的 key,激活时的顺序是用迭代器访问这个 keySet 的顺序
- Hook 里面的线程执行完了,JVM 才会退出。如果你的处理逻辑执行时间过长,可能
- 等待 Hook 线程完成,JVM 长时间不退出
外部环境预期你短时间内就会退出,提前结束了 JVM 进程,Hook 线程也就会提前终止了. 例如常见的套路
shutdown while(!timeout) check shutdownNow
第三方组件 hook 的管理
JDK 中相关的实现类是 ApplicationShutdownHooks
class ApplicationShutdownHooks { /* The set of registered hooks */ private static IdentityHashMap<Thread, Thread> hooks; // ... }
这些访问权限导致没法直接控制已经注册的 hooks,只能用到反射来操作这个 Map 了。 注意下,这个 Map key 是线程对象本身,如果没有指定线程名称就很难判断线程的作用了。 所以,指定线程的名称是个好习惯。
如果要控制顺序怎么办?这是一个跟本文无关的问题,自己想办法就行了,众所周知,控制优先级是不行的。
优雅的线程池退出
优雅的退出时碰到最多的可能就是线程池的关闭退出了,
如何才能减少对停机时线程池中正在运行的线程的影响呢?
ExecutorService
的 javadoc 中有一段两相提交关闭线程池的示例代码。抄它即可
void shutdownAndAwaitTermination(ExecutorService pool) { pool.shutdown(); // Disable new tasks from being submitted try { // Wait a while for existing tasks to terminate if (!pool.awaitTermination(60, TimeUnit.SECONDS)) { pool.shutdownNow(); // Cancel currently executing tasks // Wait a while for tasks to respond to being cancelled if (!pool.awaitTermination(60, TimeUnit.SECONDS)) System.err.println("Pool did not terminate"); } } catch (InterruptedException ie) { // (Re-)Cancel if current thread also interrupted pool.shutdownNow(); // Preserve interrupt status Thread.currentThread().interrupt(); } }
这里的 catch 块中的再次调用一下中断这个细节的处理挺优雅的。 其实不调用也没太大关系,只是线程最后的结束状态不一样而已。
spring 中的优雅停机
spring 也通过注册 JDK 的 shutdownHook 提供了 ContextClosedEvent 事件,
看 AbstractApplicationContext.registerShutdownHook
。
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 退出时被触发,这个方法在主动调用 ApplicationContext.close() 的时候也会也会被调用。 doClose 方法中会按顺序执行:
- 发布 ContextClosedEvent 事件,这个事件的处理在整个 doClose 的过程中是同步执行的
- 处理 LifeCycle 接口,SmartLifeCycle 就是它的一个子类,通常我们说的 springboot 的优雅停机 就是基于这个特性实现的。 如果你要让自己的组件优雅退出,推荐使用这个特性,例如,RocketMQ 官方提供的 client 的 starter。
- 处理 bean 的销毁,这个过程是并行的。如果指定了依赖会先销毁依赖 bean
这里是监听 ContextClosedEvent 事件时执行时的调用栈(springboot 2.1.3.RELEASE/spring 5.1.5.RELEASE)
at com.yam.demo.ShutdownListener.onApplicationEvent(ShutdownListener.java:9) at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:172) at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:165) at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:139) at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:402) at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:359) at org.springframework.context.support.AbstractApplicationContext.doClose(AbstractApplicationContext.java:1009) at org.springframework.context.support.AbstractApplicationContext$1.run(AbstractApplicationContext.java:945)
这里是监听 SmartLifeCycle 组件退出时的调用栈
at com.yam.service.SmartLifeCycleComponent.stop(SmartLifeCycleComponent.java:37) at org.springframework.context.SmartLifecycle.stop(SmartLifecycle.java:111) at org.springframework.context.support.DefaultLifecycleProcessor.doStop(DefaultLifecycleProcessor.java:238) at org.springframework.context.support.DefaultLifecycleProcessor.access$300(DefaultLifecycleProcessor.java:53) at org.springframework.context.support.DefaultLifecycleProcessor$LifecycleGroup.stop(DefaultLifecycleProcessor.java:377) at org.springframework.context.support.DefaultLifecycleProcessor.stopBeans(DefaultLifecycleProcessor.java:210) at org.springframework.context.support.DefaultLifecycleProcessor.onClose(DefaultLifecycleProcessor.java:128) at org.springframework.context.support.AbstractApplicationContext.doClose(AbstractApplicationContext.java:1018) at org.springframework.context.support.AbstractApplicationContext$1.run(AbstractApplicationContext.java:945)