一. shutdownHook的背景
想象一下,如果你现在刚好在 word 上写需求文档,电脑突然重启。等待开机完成,你可能会发现写了一个小时文档没有保存,就这么没了。。。
一个正在运行 Java 应用如果突然将其停止,影响不止数据丢失,还会造成其他影响。比如:
- 请求丢失:内存队列中等待执行请求丢失
- 数据丢失:处于内存缓存中数据未持久化到磁盘
- 文件损坏:正在写的文件没有没有更新完成,导致文件损坏
- 业务中断:处理一半的业务被强行中断,如支付成功了,却没有更新到数据库中
- 服务未下线:上游服务依然往停止节点发送请求
所以在关闭服务之前,我们需要先做好善后工作,比如保存数据,清理资源,下线服务,然后才退出应用。这种有计划平滑的关闭应用相对直接停止应用,就显得非常『优雅』。
ps: 仔细品味,优雅停机这个词真好~
二.ShutdownHook用法
Java 语言提供一种 ShutdownHook(钩子)进制,当 JVM 接受到系统的关闭通知之后,调用 ShutdownHook 内的方法,用以完成清理操作,从而平滑的退出应用。
ShutdownHook代码如下:
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
System.out.println("关闭应用,释放资源");
}));
Runtime.getRuntime().addShutdownHook(Thread) 需要传入一个线程对象,后续动作将会在该异步线程内完成。除了主动关闭应用(使用 kill -15 指令),以下场景也将会触发 ShutdownHook :
- 代码执行结束,JVM 正常退出
- 应用代码中调用
System#exit方法 - 应用中发生 OOM 错误,导致 JVM 关闭
- 终端中使用
Ctrl+C(非后台运行)
目前很多开源框架都是基于这个机制实现优雅停机,比如 Dubbo,Spring 等。
三.相关注意点
ShutdownHook 代码实现起来相对简单,但是我们还是需要小心下面这些坑。
Runtime.getRuntime().addShutdownHook(Thread) 可以被多次调用
我们可以多次调用 Runtime.getRuntime().addShutdownHook(Thread) 方法,从而增加多个。但是需要注意的是,多个 ShutdownHook 之间并无任何顺序,Java 并不会按照加入顺序执行,反而将会并发执行。
所以尽量在一个 ShutdownHook 完成所有操作。
ShutdownHook 需要尽快执行结束
不要在 ShutdownHook 执行需要被阻塞代码,如 I/0 读写,这样就会导致应用短时间不能被关闭。
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
while (true){
System.out.println("关闭应用,释放资源");
}
}));
上面代码中,我们使用 while(true) 模拟长时间阻塞这种极端情况,关闭该应用时,应用将会一直阻塞在 while代码中,导致应用没办法被关闭。
除了阻塞之外,还需要小心其他会让线程阻塞的行为,比如死锁。
为了避免 ShutdownHook 线程被长时间阻塞,我们可以引入超时进制。如果等待一定时间之后,ShutdownHook 还未完成,由脚本直接调用 kill -9 强制退出或者 ShutdownHook 代码中引入超时进制。
1. 不要使用kill -9来结束你的应用程序
kill -9是操作系统的终极杀器,如果你使用kill -9的话,应用程序是没有任何发言权的,它只能选择默默退出,几乎可以肯定,shutdownHook不会被调用。
2. shutdownHook涉及的方法应该尽量的短
这个可能和操作系统有关系,不同的操作系统可能有不同的差异:
When a computer shuts down, the final stage of the shutdown process sends every remaining process a SIGTERM, gives those processes a few seconds grace, then sends them a SIGKILL.
也就是说,如果shutdownHook过长,可能方法还没执行完,进程就被操作系统强制杀掉了,这一点在addShutdownHook()的文档上也有提及:
* <p> Shutdown hooks should also finish their work quickly. When a
* program invokes {@link #exit exit} the expectation is
* that the virtual machine will promptly shut down and exit. When the
* virtual machine is terminated due to user logoff or system shutdown the
* underlying operating system may only allow a fixed amount of time in
* which to shut down and exit. It is therefore inadvisable to attempt any
* user interaction or to perform a long-running computation in a shutdown
* hook.
3. shutdownHook的方法应该是线程安全的
这是因为,用户可能多次发送信号导致方法被不同的线程被多次调用,关于这一点文档也有说明:
* They should, in
* particular, be written to be thread-safe and to avoid deadlocks insofar
* as possible.
4. 关于shutdownHook方法的异常
shutdownHook调用过程中产生的所有异常都会被忽略掉并且可能不会输出任何提示信息,因此程序可能蕴含了一个久久不能被发现的BUG导致你的shutdownHook无法被执行,在调用shutdownHook的过程中,一定要仔细检查你的代码,保证正确性。
5. 某些场景下要提供at most once的保证
这点其实是接第三点说的,就是你的shutdownHook可能被调用多次,但其实关闭一次就够了,多次调用可能会引发一些意想不到的异常。比如KafkaStream的close方法,就提供了这样的保证:
public synchronized boolean close(long timeout, TimeUnit timeUnit) {
this.log.debug("Stopping Streams client with timeoutMillis = {} ms.", timeUnit.toMillis(timeout));
if (!this.setState(KafkaStreams.State.PENDING_SHUTDOWN)) {
this.log.info("Already in the pending shutdown state, wait to complete shutdown");
} else {
// ....
可以使用CAS操作来做这样的检查:
if (state.compareAndSet(ACTIVE, CLOSED)) {
// close here
}
以上就是我在使用shutdownHook的过程中总结的一些点;同时强烈建议大家在使用shutdownHook前仔细阅读该方法的文档以及我前面提到的Stack Overflow上的回答。
四.源码分析实现原理
实现原理:
Runtime.getRuntime().addShutdownHook(hook); // 添加钩子,开启优雅之路
// 具体流程如下:
/**
* Registers a new virtual-machine shutdown hook.
*
* @param hook
* An initialized but unstarted <tt>{@link Thread}</tt> object
*
* @throws IllegalArgumentException
* If the specified hook has already been registered,
* or if it can be determined that the hook is already running or
* has already been run
*
* @throws IllegalStateException
* If the virtual machine is already in the process
* of shutting down
*
* @throws SecurityException
* If a security manager is present and it denies
* <tt>{@link RuntimePermission}("shutdownHooks")</tt>
*
* @see #removeShutdownHook
* @see #halt(int)
* @see #exit(int)
* @since 1.3
*/
public void addShutdownHook(Thread hook) {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(new RuntimePermission("shutdownHooks"));
}
// 添加到 application 中
ApplicationShutdownHooks.add(hook);
}
// java.lang.ApplicationShutdownHooks.add(hook);
static synchronized void add(Thread hook) {
if(hooks == null)
throw new IllegalStateException("Shutdown in progress");
if (hook.isAlive())
throw new IllegalArgumentException("Hook already running");
if (hooks.containsKey(hook))
throw new IllegalArgumentException("Hook previously registered");
// hooks 以map类型保存, k->k 形式存储,保证每一个钩子都是独立的
hooks.put(hook, hook);
}
// java.lang.ApplicationShutdownHooks 会先注册一个静态块,添加一个任务到 Shutdown 中
/* The set of registered hooks */
private static IdentityHashMap<Thread, Thread> hooks;
static {
try {
Shutdown.add(1 /* shutdown hook invocation order */,
false /* not registered if shutdown in progress */,
new Runnable() {
public void run() {
// 即当该任务被调用时,调用自身的运行方法,使所有注册的 hook 运行起来
runHooks();
}
}
);
hooks = new IdentityHashMap<>();
} catch (IllegalStateException e) {
// application shutdown hooks cannot be added if
// shutdown is in progress.
hooks = null;
}
}
// runHooks 执行所有钩子线程,进行异步调用
/* 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<Thread> threads;
synchronized(ApplicationShutdownHooks.class) {
threads = hooks.keySet();
hooks = null;
}
for (Thread hook : threads) {
hook.start();
}
for (Thread hook : threads) {
try {
// 阻塞等待所有完成
hook.join();
} catch (InterruptedException x) { }
}
}
到现在为止,我们已经知道关闭钩子是如何执行的,但是,还不是知道,该钩子是何时触发?
// java.lang.Shutdown.add() 该方法会jvm主动调用,从而触发 后续钩子执行
/* Invoked by the JNI DestroyJavaVM procedure when the last non-daemon
* thread has finished. Unlike the exit method, this method does not
* actually halt the VM.
*/
static void shutdown() {
synchronized (lock) {
switch (state) {
case RUNNING: /* Initiate shutdown */
state = HOOKS;
break;
case HOOKS: /* Stall and then return */
case FINALIZERS:
break;
}
}
synchronized (Shutdown.class) {
// 执行序列
sequence();
}
}
// 而 sequence() 则会调用 runHooks(), 调用自定义的钩子任务
private static void sequence() {
synchronized (lock) {
/* Guard against the possibility of a daemon thread invoking exit
* after DestroyJavaVM initiates the shutdown sequence
*/
if (state != HOOKS) return;
}
runHooks();
boolean rfoe;
synchronized (lock) {
state = FINALIZERS;
rfoe = runFinalizersOnExit;
}
if (rfoe) runAllFinalizers();
}
// 执行钩子,此处最多允许注册 10 个钩子,且进行同步调用,当然这是最顶级的钩子,钩子下还可以添加钩子,可以任意添加n个
private static void runHooks() {
for (int i=0; i < MAX_SYSTEM_HOOKS; i++) {
try {
Runnable hook;
synchronized (lock) {
// acquire the lock to make sure the hook registered during
// shutdown is visible here.
currentRunningHook = i;
hook = hooks[i];
}
// 同步调用注册的hook, 即 前面看到 ApplicationShutdownHooks.runHooks()
if (hook != null) hook.run();
} catch(Throwable t) {
if (t instanceof ThreadDeath) {
ThreadDeath td = (ThreadDeath)t;
throw td;
}
}
}
}
如此,整个关闭流程完美了。
简化为:
1. 注册流程(应用主动调用)
Runtime.addShutdownHook -> ApplicationShutdownHooks.add()/static -> java.lang.Shutdown.add()/shutdown()
2. 执行流程(jvm自动调用)
java.lang.Shutdown.shutdown()->sequence()->runHooks() -> ApplicationShutdownHooks.runHooks() -> hooks 最终
浙公网安备 33010602011771号