• java8:关闭钩子shutdown hook


    参考资料:

    《Java中的关闭钩子(shutdown hook)》

    《JVM 的关闭钩子》

            写在开头:本文为学习后的总结,可能有不到位的地方,错误的地方,欢迎各位指正。

    目录

    一、关闭钩子

            1、基本介绍

            2、如何使用

    二、源码分析

            注册

            执行

    三、补充

            1、屏敝第三方组件的ShutdownHook

            2、关闭公共组件时的注意点


    一、关闭钩子

            1、基本介绍

            在Java程序退出时,我们可能需要先执行一些善后工作,如关闭线程池、连接池、文件句柄等,即所谓“优雅停机”(graceful shutdown)。如何保证善后工作的代码能够被执行到呢?Java为用户提供了关闭钩子(shutdown hook)

            这些钩子可以在应用关闭时帮助我们完成JVM退出前的善后工作,例如删除临时文件,或者清除无法由操作系统自动清除的资源

            关闭钩子在以下情景都会被调用:

    • 程序正常退出,即最后一个非守护线程结束时;
    • 程序中执行到了System.exit()方法;
    • 终端接收到了CTRL-C中断,或者注销登录;
    • 通过kill命令杀死进程(但是kill -9除外)。

            关闭钩子在以下情景不会被调用:

    • 通过kill -9命令杀死进程——所以kill -9一定要慎用;
    • 程序中执行到了Runtime.getRuntime().halt()方法;
    • 操作系统突然崩溃,或机器掉电。

            2、如何使用

            我们可以通过Runtime.getRuntime().addShutdownHook()方法来注册关闭钩子:

    1. Runtime.getRuntime().addShutdownHook(new Thread(() -> {
    2. System.out.println("auto clean temporary file");
    3. }));

            注意:这里的钩子方法必须是Thread的子类,换句话说,钩子方法是异步执行的。

            我们通过下面的例子演示下多个钩子方法的执行:

    1. public class T {
    2. @SuppressWarnings("deprecation")
    3. public static void main(String[] args) throws Exception {
    4. MyHook hook1 = new MyHook("Hook1");
    5. MyHook hook2 = new MyHook("Hook2");
    6. MyHook hook3 = new MyHook("Hook3");
    7. //注册关闭钩子
    8. Runtime.getRuntime().addShutdownHook(hook1);
    9. Runtime.getRuntime().addShutdownHook(hook2);
    10. Runtime.getRuntime().addShutdownHook(hook3);
    11. //移除关闭钩子
    12. Runtime.getRuntime().removeShutdownHook(hook3);
    13. //Main线程将在执行这句之后退出
    14. System.out.println("Main Thread Ends.");
    15. }
    16. }
    17. class MyHook extends Thread {
    18. private String name;
    19. public MyHook (String name) {
    20. this.name = name;
    21. setName(name);
    22. }
    23. public void run() {
    24. System.out.println(name + " Ends.");
    25. }
    26. }

            可以看到,main函数执行完成,首先输出的是Main Thread Ends,接下来执行关闭钩子,输出Hook2 Ends和Hook1 Ends。这两行也可以证实:关闭钩子的本质就是已经初始化但在JVM关闭之前最后一刻才会执行的线程,并且JVM不是以注册的顺序来调用关闭钩子的。而由于hook3在调用了addShutdownHook后,接着对其调用了removeShutdownHook将其移除,于是hook3在JVM退出时没有执行,因此没有输出Hook3 Ends。

    1. Main Thread Ends.
    2. Hook2 Ends.
    3. Hook1 Ends.

    二、源码分析

            注册

            我们先从注册钩子的源码开始分析,于是我们找到Runtime类中的addShutdownHook()方法。

    1. public void addShutdownHook(Thread hook) {
    2. SecurityManager sm = System.getSecurityManager();
    3. if (sm != null) {
    4. sm.checkPermission(new RuntimePermission("shutdownHooks"));
    5. }
    6. ApplicationShutdownHooks.add(hook);
    7. }

            addShutdownHook()方法实际上是代理了ApplicationShutdownHooks.add()方法。

            ApplicationShutdownHooks类会初始化一个map容器来存储注册进来的钩子。在注册关闭钩子之前,会先判断是否符合以下三个条件,如果是,则钩子无法注册:

    • JVM正在关闭,即钩子已经被触发(此时IdentityHashMap为null);
    • 当前关闭钩子正在执行;
    • IdentityHashMap中已经存在了要注册的钩子。
    1. class ApplicationShutdownHooks {
    2. // 用来存放钩子的容器
    3. private static IdentityHashMap hooks;
    4. private ApplicationShutdownHooks() {}
    5. // 注册方法
    6. static synchronized void add(Thread hook) {
    7. // JVM正在关闭,即钩子已经被触发(此时IdentityHashMap为null)
    8. if(hooks == null)
    9. throw new IllegalStateException("Shutdown in progress");
    10. // 钩子是否已在运行
    11. if (hook.isAlive())
    12. throw new IllegalArgumentException("Hook already running");
    13. // 判断是否重复注册
    14. if (hooks.containsKey(hook))
    15. throw new IllegalArgumentException("Hook previously registered");
    16. hooks.put(hook, hook);
    17. }
    18. // 删除注册
    19. static synchronized boolean remove(Thread hook) {
    20. if(hooks == null)
    21. throw new IllegalStateException("Shutdown in progress");
    22. if (hook == null)
    23. throw new NullPointerException();
    24. return hooks.remove(hook) != null;
    25. }
    26. // 其余方法
    27. }

            执行

            启动钩子的方法则是ApplicationShutdownHooks.runHooks(),当被调用时,该方法会挂起调用线程,等待被调用线程(在这里就是钩子线程)执行完毕之后,调用线程才继续执行。也就是说,这里必须保证关闭钩子在主线程真正关闭之前执行完毕。

    1. static void runHooks() {
    2. Collection threads;
    3. synchronized(ApplicationShutdownHooks.class) {
    4. // 获取所有的钩子
    5. threads = hooks.keySet();
    6. hooks = null;
    7. }
    8. for (Thread hook : threads) {
    9. // 异步启动
    10. hook.start();
    11. }
    12. for (Thread hook : threads) {
    13. while (true) {
    14. try {
    15. // 挂起调用线程
    16. hook.join();
    17. break;
    18. } catch (InterruptedException ignored) {
    19. }
    20. }
    21. }
    22. }

            runHooks()由ApplicationShutdownHooks方法的static代码块中注册为了Runnable,没有立即执行。

    1. static {
    2. try {
    3. Shutdown.add(1 /* shutdown hook invocation order */,
    4. false /* not registered if shutdown in progress */,
    5. new Runnable() {
    6. public void run() {
    7. runHooks();
    8. }
    9. }
    10. );
    11. hooks = new IdentityHashMap<>();
    12. } catch (IllegalStateException e) {
    13. // application shutdown hooks cannot be added if
    14. // shutdown is in progress.
    15. hooks = null;
    16. }
    17. }

            Shutdown类内用Runnable的数组hooks维护关闭钩子的执行,并且该数组同时表示关闭钩子的优先级,排在前面slot的会先执行。虽然该数组的长度为10,但是目前只用了3个slot,用户注册的应用关闭钩子的优先级夹在两种系统钩子的中间(即固定占用slot 1)。

            registerShutdownInProgress表示是否允许在关闭过程中注册钩子,前面传入的是false。如果为true的话,则可以在当前运行的钩子后面注册优先级更低的钩子。

    1. private static final int RUNNING = 0;
    2. private static final int HOOKS = 1;
    3. private static final int FINALIZERS = 2;
    4. private static int state = RUNNING;
    5. // The system shutdown hooks are registered with a predefined slot.
    6. // The list of shutdown hooks is as follows:
    7. // (0) Console restore hook
    8. // (1) Application hooks
    9. // (2) DeleteOnExit hook
    10. private static final int MAX_SYSTEM_HOOKS = 10;
    11. private static final Runnable[] hooks = new Runnable[MAX_SYSTEM_HOOKS];
    12. static void add(int slot, boolean registerShutdownInProgress, Runnable hook) {
    13. synchronized (lock) {
    14. if (hooks[slot] != null)
    15. throw new InternalError("Shutdown hook at slot " + slot + " already registered");
    16. // 是否允许在关闭过程中注册钩子
    17. if (!registerShutdownInProgress) {
    18. if (state > RUNNING)
    19. throw new IllegalStateException("Shutdown in progress");
    20. } else {
    21. if (state > HOOKS || (state == HOOKS && slot <= currentRunningHook))
    22. throw new IllegalStateException("Shutdown in progress");
    23. }
    24. hooks[slot] = hook;
    25. }
    26. }

            JVM关闭时将逐步调用到shutdown类的exit方法。

    1. // System.java
    2. public static void exit(int status) {
    3. Runtime.getRuntime().exit(status);
    4. }
    5. // Runtime.java
    6. public void exit(int status) {
    7. SecurityManager security = System.getSecurityManager();
    8. if (security != null) {
    9. security.checkExit(status);
    10. }
    11. Shutdown.exit(status);
    12. }

            此时会执行到sequence()方法,钩子方法的调用正式改方法内执行的。

    1. static void exit(int status) {
    2. // 其余代码
    3. synchronized (Shutdown.class) {
    4. sequence();
    5. halt(status);
    6. }
    7. }
    8. // 调用钩子方法
    9. private static void sequence() {
    10. synchronized (lock) {
    11. if (state != HOOKS) return;
    12. }
    13. runHooks();
    14. boolean rfoe;
    15. synchronized (lock) {
    16. state = FINALIZERS;
    17. rfoe = runFinalizersOnExit;
    18. }
    19. if (rfoe) runAllFinalizers();
    20. }
    21. // 真正关闭JVM的方法
    22. static void halt(int status) {
    23. synchronized (haltLock) {
    24. halt0(status);
    25. }
    26. }

    三、补充

            1、屏敝第三方组件的ShutdownHook

            有一些第三方组件在代码中注册了关闭自身资源的ShutdownHook,这些ShutdownHook对于我们的平滑退出有时候起了反作用。

            从Runtime.java和ApplicationShutdownHooks.java的源码中看到ApplicationShutdownHooks不是public的,类中的hooks也是private的。只有通过反射的方式才能获取并控制它们。定义ExcludeIdentityHashMap类来帮助我们阻止非自己的ShutdownHook注入。

    1. class ExcludeIdentityHashMap extends IdentityHashMap {
    2. public V put(K key, V value) {
    3. if (key instanceof Thread) {
    4. Thread thread = (Thread) key;
    5. if (!thread.getName().startsWith("My-")) {
    6. return value;
    7. }
    8. }
    9. return super.put(key, value);
    10. }
    11. }

            通过反射的方式注入自己的ShutdownHook并清除其他Thread

    1. String className = "java.lang.ApplicationShutdownHooks";
    2. Class clazz = Class.forName(className);
    3. Field field = clazz.getDeclaredField("hooks");
    4. field.setAccessible(true);
    5. Thread shutdownThread = new Thread(new Runnable() {
    6. @Override
    7. public void run() {
    8. // TODO
    9. }
    10. });
    11. shutdownThread.setName("My-WebShutdownThread");
    12. IdentityHashMap excludeIdentityHashMap = new ExcludeIdentityHashMap<>();
    13. excludeIdentityHashMap.put(shutdownThread, shutdownThread);
    14. synchronized (clazz) {
    15. IdentityHashMap map = (IdentityHashMap) field.get(clazz);
    16. for (Thread thread : map.keySet()) {
    17. Log.info("found shutdownHook: " + thread.getName());
    18. excludeIdentityHashMap.put(thread, thread);
    19. }
    20. field.set(clazz, excludeIdentityHashMap);
    21. }

            2、关闭公共组件时的注意点

            如果在关闭钩子中关闭应用程序的公共的组件,如日志服务,或者数据库连接等,像下面这样:

    1. Runtime.getRuntime().addShutdownHook(new Thread() {
    2. public void run() {
    3. try {
    4. LogService.this.stop();
    5. } catch (InterruptedException ignored){
    6. //ignored
    7. }
    8. }
    9. });

            由于关闭钩子将并发执行,因此在关闭日志时可能导致其他需要日志服务的关闭钩子产生问题。为了避免这种情况,可以使关闭钩子不依赖那些可能被应用程序或其他关闭钩子关闭的服务。        

            实现这种功能的一种方式是对所有服务使用同一个关闭钩子(而不是每个服务使用一个不同的关闭钩子),并且在该关闭钩子中执行一系列的关闭操作。这确保了关闭操作在单个线程中串行执行,从而避免了在关闭操作之前出现竞态条件或死锁等问题。

  • 相关阅读:
    Redis——》数据类型
    JS作用域
    Docker Swarm集群部署
    Java语言中的文件数据流
    Clickhouse 索引原理
    突破编程_C++_面试(STL 编程 vector )
    【刷题专项】— 模拟
    名称服务器(Name Server)介绍
    什么是集成测试?集成测试方法有哪些?
    Thread类及常见方法
  • 原文地址:https://blog.csdn.net/wzngzaixiaomantou/article/details/128045692