前言

在看Spring应用上下文生命周期时,其中AbstractApplicationContext抽象类中有个registerShutdownHook方法,提到了Runtime.getRuntime().addShutdownHook(),顺便记录下。


介绍

Runtime.getRuntime().addShutdownHook() 通过给jvm增加一个钩子,当jvm进行关闭时会调用添加的Thread任务,自定义清除工作,使得jvm能够进行优雅的关闭。

如果在某些场景下强制退出,例如使用kill -9可能会导致一些问题,如下:
1、缓存中数据尚未持久化,致使数据丢失
2、异步任务未执行完成,致使任务丢失
3、正在写入的文件,没有更新完成,致使文件损坏


示例

模拟jvm退出,保证线程池提交的任务执行完成。

创建测试类ShutdownHookTest

public class ShutdownHookTest {

    private static ScheduledExecutorService executorService = Executors.newScheduledThreadPool(3);
    static {
        // jvm关闭的钩子
        Runtime.getRuntime().addShutdownHook(new Thread(() -> close()));
    }


    /**
     * 回调关闭
     */
    private static void close() {
        System.out.println("======start close======");
        executorService.shutdown();
        System.out.println("executorService#shutdown");
        try {
            // 最多等10秒,awaitTermination return true if this executor terminated and false if the timeout elapsed before termination
            System.out.println("executorService#awaitTermination " + executorService.awaitTermination(10000, TimeUnit.SECONDS));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("======end close======");
    }

    public static void main(String[] args) throws Exception {
        // 测试kill -9的情况,直接结束进程,ShutdownHook得不到执行
         displayProcessId(ShutdownHookTest.class);

        // 每1秒提交一个3秒后执行的任务
        for (int i = 0 ; i < 10; i ++) {
            Thread.sleep(1000);

            final int index = i;
            // 3s后执行任务 先shutdown再terminated
            if (!executorService.isShutdown()) {
                System.out.println("add execute " + index);
                executorService.schedule(() -> System.out.println("execute " + index),
                        3L, TimeUnit.SECONDS);
            }

            // 第5秒时,关闭进程
            if (i == 5) {
                System.exit(0);
            }
        }
    }

    public static void displayProcessId(Class clazz) throws Exception {
        // 获取监控主机
        MonitoredHost local = MonitoredHost.getMonitoredHost("localhost");
        // 取得所有在活动的虚拟机集合
        Set<?> vmList = new HashSet<Object>(local.activeVms());
        // 遍历集合,输出PID和进程名
        for(Object process : vmList) {
            MonitoredVm vm = local.getMonitoredVm(new VmIdentifier("//" + process));
            // 获取类名
            String processName = MonitoredVmUtil.mainClass(vm, true);
            if (clazz.getName().equals(processName)) {
                System.out.println(process + " ------> " + processName);
            }
        }

    }

}

示例中,通过Runtime.getRuntime().addShutdownHook(new Thread(() -> close()));添加ShutdownHook任务,当jvm关闭时,回调close方法。

close方法会进行线程池的关闭,同时等待10s,使得未执行的任务留有10s时间进行执行。

在主方法中,「每1秒提交一个3秒后执行的任务」,预创建10个,当创建第5个时,调用System.exit(0);关闭应用进程。

输出结果如下:

add execute 0
add execute 1
add execute 2
execute 0
add execute 3
execute 1
add execute 4
execute 2
add execute 5
======start close======
executorService#shutdown
execute 3
execute 4
execute 5
executorService#awaitTermination true
======end close======

另外,通过displayProcessId方法,获取到进程id,通过kill -9方法结束,此时shutdownHook任务得不到执行。

扩展

jvm关闭的常见方式,正常和异常关闭会回调shutdownHook任务。
image


总结

平时写crud关注的比较少,一般运维层面,会在某应用重启前,先将流量切到其它应用(副本),再等待一段时间后关闭并重启该应用,相当于给应用一个关闭时间,执行未完成的线程任务。
这个时长不好控制,可能30-60s,也可以结合添加shutdownHook进行资源或任务校验。或者一些场景,例如jvm关闭时清除临时资源、通知等等。