我们关闭Java服务时,应该进行一些善后工作,比如,关闭线程池,释放连接资源等,而不是直接关闭,放弃未完成的任务。
Java中,我们可以通过Runtime.getRuntime().addShutdownHook()方法来注册钩子,以保证程序平滑退出。
举个例子:
public class Demo8_shutdown {
/**
* 使用线程池处理任务
*/
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 thread = 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!");
}
});
thread.setDaemon(true);
executorService.submit(thread);
}
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);
}
}
结果:
main method exit...
1543912844206 : thread_0 start...
1543912844206 : thread_0 finish!
1543912844206 : thread_1 start...
1543912844207 : thread_2 start...
1543912844207 : thread_3 start...
1543912844208 : thread_4 start...
1543912844208 : Thread-5 No1 shutdown hooking...
1543912844209 : Thread-5 shutdown signal got, wait threadPool finish.
1543912844209 : Thread-6 No2 shutdown hooking...
1543912845240 : thread_1 finish!
1543912845240 : Thread-6 No2 shutdown done...
1543912846208 : thread_2 finish!
1543912847208 : thread_3 finish!
1543912848208 : thread_4 finish!
1543912848208 : Thread-5 all thread's done.
1543912848208 : Thread-5 No1 shutdown done...
Process finished with exit code 0
等待线程完成了任务再关闭退出服务了。
实际应用
我们平常也有遇到 直接 kill -15 pid 来关闭服务的。
通过该命令发送一个关闭信号给到jvm, 然后就开始执行 Shutdown Hook
但也有看到 kill -9 pid 这种,这种相当于直接系统宕机,没有给应用一点时间反应就杀死它了。
我们项目大多使用了Spring,Spring也提供了完善的优雅关闭的方法。
继承 ApplicationListener 监听器,监听 ContextClosedEvent事件,这样在关闭服务之前,你可以做一下销毁,善后的工作。
@Service
public class AppServerListener implements ApplicationListener<ApplicationContextEvent>{
public void onApplicationEvent(ApplicationContext event){
if(event instanceof ContextClosedEvent){
// dosomething
}
}
}
它的原理同样的也离不开 Runtime.getRuntime().addShutdownHook() 钩子方法
可看 org.springframework.context.support.AbstractApplicationContext#doClose 方法
publishEvent(new ContextClosedEvent(this)); 发布ContextClosedEvent 事件的 doclose方法
调用doclose方法的有两个途径:
1. org.springframework.context.support.AbstractApplicationContext#close
2.org.springframework.context.support.AbstractApplicationContext#registerShutdownHook
其中 第一个是 当容器关闭的时候 如果实现了 DisposableBean
方法,会调用 destroy()
方法。destroy调用 close方法。
第二种直接显示 调用 registerShutdownHook方法。
线程池的关闭方式为:
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运行了死循环代码。
实现原理
public void addShutdownHook(Thread hook) {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(new RuntimePermission("shutdownHooks"));
}
ApplicationShutdownHooks.add(hook);
}
ApplicationShutdownHooks类:
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.put(hook, hook); // hooks 以map类型保存, k->k 形式存储,保证每一个钩子都是独立的
}
// 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;
}
}
}
}
整个流程就这样下来。