哦,这就是java的优雅停机?(实现及原理)
其实优雅停机,就是在要关闭服务之前,不是立马全部关停,而是做好一些善后操作,比如:关闭线程、释放连接资源等。
再比如,就是不会让调用方的请求处理了一增,一下就中断了。而处理完本次后,再停止服务。
Java语言中,我们可以通过Runtime.getRuntime().addShutdownHook()方法来注册钩子,以保证程序平滑退出。(其他语言也类似)
来个栗子:
-
public class ShutdownGracefulTest { -
-
/** -
* 使用线程池处理任务 -
*/ -
public static ExecutorService executorService = Executors.newCachedThreadPool(); -
-
public static void main(String[] args) { -
-
//假设有5个线程需要执行任务 -
for(int i = 0; i < 5; i++){ -
final int id = i; -
Thread taski = new Thread(new Runnable() { -
@Override -
public void run() { -
System.out.println(System.currentTimeMillis() + " : thread_" + id + " start..."); -
try { -
TimeUnit.SECONDS.sleep(id); -
} -
catch (InterruptedException e) { -
e.printStackTrace(); -
} -
System.out.println(System.currentTimeMillis() + " : thread_" + id + " finish!"); -
} -
}); -
taski.setDaemon(true); -
executorService.submit(taski); -
} -
-
Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() { -
@Override -
public void run() { -
-
System.out.println(System.currentTimeMillis() + " : " + Thread.currentThread().getName() + " No1 shutdown hooking..."); -
boolean shutdown = true; -
try { -
executorService.shutdown(); -
System.out.println(System.currentTimeMillis() + " : " + Thread.currentThread().getName() + " shutdown signal got, wait threadPool finish."); -
executorService.awaitTermination(1500, TimeUnit.SECONDS); -
boolean done = false; -
System.out.println(System.currentTimeMillis() + " : " + Thread.currentThread().getName() + " all thread's done."); -
} -
catch (InterruptedException e) { -
e.printStackTrace(); -
// 尝试再次关闭 -
if(!executorService.isTerminated()) { -
executorService.shutdownNow(); -
} -
} -
System.out.println(System.currentTimeMillis() + " : " + Thread.currentThread().getName() + " No1 shutdown done..."); -
} -
})); -
-
Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() { -
@Override -
public void run() { -
try { -
System.out.println(System.currentTimeMillis() + " : " + Thread.currentThread().getName() + " No2 shutdown hooking..."); -
Thread.sleep(1000); -
} -
catch (InterruptedException e) { -
e.printStackTrace(); -
} -
System.out.println(System.currentTimeMillis() + " : " + Thread.currentThread().getName() + " No2 shutdown done..."); -
} -
})); -
-
System.out.println("main method exit..."); -
System.exit(0); -
} -
}
运行结果如下:
很明显,确实是优雅了,虽然最后收到了一关闭信号,但是仍然保证了任务的处理完成。很棒吧!
那么,在实际应用中是如何体现优雅停机呢?
kill -15 pid
通过该命令发送一个关闭信号给到jvm, 然后就开始执行 Shutdown Hook 了,你可以做很多:
-
关闭 socket 链接
-
清理临时文件
-
发送消息通知给订阅方,告知自己下线
-
将自己将要被销毁的消息通知给子进程
-
各种资源的释放 ...
而在平时工作中,我们不乏看到很多运维同学,是这么干的:
kill -9 pid
如果这么干的话,jvm也无法了,kill -9 相当于一次系统宕机,系统断电。这会给应用杀了个措手不及,没有留给应用任何反应的机会。
所以,无论如何是优雅不起来了。
要优雅,是代码和运维的结合!
其中,线程池的关闭方式为:
-
executorService.shutdown(); -
executorService.awaitTermination(1500, TimeUnit.SECONDS);
ThreadPoolExecutor 在 shutdown 之后会变成 SHUTDOWN 状态,无法接受新的任务,随后等待正在执行的任务执行完成。意味着,shutdown 只是发出一个命令,至于有没有关闭还是得看线程自己。
ThreadPoolExecutor 对于 shutdownNow 的处理则不太一样,方法执行之后变成 STOP 状态,并对执行中的线程调用 Thread.interrupt() 方法(但如果线程未处理中断,则不会有任何事发生),所以并不代表“立刻关闭”。
-
shutdown() :启动顺序关闭,其中执行先前提交的任务,但不接受新任务。如果已经关闭,则调用没有附加效果。此方法不等待先前提交的任务完成执行。
-
shutdownNow():尝试停止所有正在执行的任务,停止等待任务的处理,并返回正在等待执行的任务的列表。当从此方法返回时,这些任务将从任务队列中耗尽(删除)。此方法不等待主动执行的任务终止。
-
executor.awaitTermination(this.awaitTerminationSeconds, TimeUnit.SECONDS)); 控制等待的时间,防止任务无限期的运行(前面已经强调过了,即使是 shutdownNow 也不能保证线程一定停止运行)。
注意:
-
虚拟机会对多个shutdownhook以未知的顺序调用,都执行完后再退出。
-
如果接收到 kill -15 pid 命令时,执行阻塞操作,可以做到等待任务执行完成之后再关闭 JVM。同时,也解释了一些应用执行 kill -15 pid 无法退出的问题,如:中断被阻塞了,或者hook运行了死循环代码。
实现原理:
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; -
} -
} -
} -
}
如此,整个关闭流程完美了。
简化为:
-
注册流程(应用主动调用):Runtime.addShutdownHook -> ApplicationShutdownHooks.add()/static -> java.lang.Shutdown.add()/shutdown()
-
执行流程(jvm自动调用):java.lang.Shutdown.shutdown()->sequence()->runHooks() -> ApplicationShutdownHooks.runHooks() -> hooks 最终