Java关闭钩子

背景

在JVM退出时,我们有时候希望系统帮忙完成一些清场工作,例如状态同步,系统资源释放等等。JAVA中的ShutdownHook提供了比较好的方案。

什么时shutdownHook?

Shutdown hook是一个initialized but unstarted thread。当JVM开始执行shutdown sequence时,会并发运行所有registered Shutdown Hook。这时,在Shutdown Hook这个线程里定义的操作便会开始执行。需要注意的是,在Shutdown Hook里执行的操作应当是不太耗时的。因为在用户注销或者操作系统关机导致的JVM shutdown的例子中,系统只会预留有限的时间给未完成的工作,超时之后还是会强制关闭。


JDK提供了Java.Runtime.addShutdownHook(Thread hook)方法,可以注册一个JVM关闭的钩子,这个钩子可以在一下几种场景中被调用:

  1. 程序正常退出
  2. 使用System.exit()
  3. 终端使用Ctrl+C触发的中断
  4. 系统关闭
  5. OutOfMemory宕机
  6. 使用Kill pid命令干掉进程(注:在使用kill -9 pid时,是不会被调用的

如何使用Shutdown Hook

调用java.lang.Runtime这个类的addShutdownHook(Thread hook)方法即可注册一个Shutdown Hook,然后在Thread中定义需要在system exit时进行的操作。如下:

Runtime.getRuntime().addShutdownHook(new Thread(() -> System.out.println("Do something in Shutdown Hook")));
实例代码如下:
 1 package com.demo;
 2 
 3 public class ShutdownHookTest {
 4 
 5     public static void main(String[] args) {
 6         Thread hook = new Thread(new Hook("A"));
 7         Runtime.getRuntime().addShutdownHook(hook);
 8         hook = new Thread(new Hook("B"));
 9         Runtime.getRuntime().addShutdownHook(hook);
10         hook = new Thread(new Hook("C"));
11         Runtime.getRuntime().addShutdownHook(hook);
12         hook = new Thread(new Hook("D"));
13         Runtime.getRuntime().addShutdownHook(hook);
14         int count = 0;
15         while (count < 10) {
16             new Thread(new NomalThread()).start();
17             count++;
18         }
19     }
20 }
21 
22 class Hook implements Runnable {
23 
24     private String hookName;
25 
26     public Hook(String hookName) {
27         super();
28         this.hookName = hookName;
29     }
30 
31     @Override
32     public void run() {
33         System.out.println("i am the shutdown hook[" + hookName + "]");
34     }
35 
36 }
37 
38 class NomalThread implements Runnable {
39 
40     @Override
41     public void run() {
42         System.out.println(String.format("Thread %s running", Thread.currentThread().getName()));
43         try {
44             Thread.sleep(1000);
45         } catch (InterruptedException e) {
46             e.printStackTrace();
47         }
48     }
49 
50 }
View Code

源码解读

  1 //java.lang.Runtime
  2 public void addShutdownHook(Thread hook) {
  3         SecurityManager sm = System.getSecurityManager();
  4         if (sm != null) {
  5             sm.checkPermission(new RuntimePermission("shutdownHooks"));
  6         }
  7         ApplicationShutdownHooks.add(hook);
  8     }
  9 
 10 //java.lang.ApplicationShutdownHooks
 11 /* Add a new shutdown hook.  Checks the shutdown state and the hook itself,
 12      * but does not do any security checks.
 13      */
 14     static synchronized void add(Thread hook) {
 15         if(hooks == null)
 16             throw new IllegalStateException("Shutdown in progress");
 17 
 18         if (hook.isAlive())
 19             throw new IllegalArgumentException("Hook already running");
 20 
 21         if (hooks.containsKey(hook))
 22             throw new IllegalArgumentException("Hook previously registered");
 23 
 24         hooks.put(hook, hook);
 25     }
 26 
 27 //hooks有什么用?
 28 /* Iterates over all application hooks creating a new thread for each
 29      * to run in. Hooks are run concurrently and this method waits for
 30      * them to finish.
 31      */
 32     static void runHooks() {
 33         Collection<Thread> threads;
 34         synchronized(ApplicationShutdownHooks.class) {
 35             threads = hooks.keySet();
 36             hooks = null;
 37         }
 38 
 39         for (Thread hook : threads) {
 40             hook.start();
 41         }
 42         for (Thread hook : threads) {
 43             while (true) {
 44                 try {
 45                     hook.join();
 46                     break;
 47                 } catch (InterruptedException ignored) {
 48                 }
 49             }
 50         }
 51     }
 52 
 53 
 54 /*
 55 这些钩子会在ApplicationShutdownHooks的初始化的时候,在static块里面被添加到Shudown的hooks里面
 56 java.lang.ApplicationShutdownHooks
 57 */
 58 static {
 59         try {
 60             Shutdown.add(1 /* shutdown hook invocation order */,
 61                 false /* not registered if shutdown in progress */,
 62                 new Runnable() {
 63                     public void run() {
 64                         runHooks();
 65                     }
 66                 }
 67             );
 68             hooks = new IdentityHashMap<>();
 69         } catch (IllegalStateException e) {
 70             // application shutdown hooks cannot be added if
 71             // shutdown is in progress.
 72             hooks = null;
 73         }
 74     }
 75 // 注意,Shutdown的hooks和ApplicationShutdownHooks.hooks不同
 76 //添加到Shutdown的hooks
 77 /**
 78      * Add a new system shutdown hook.  Checks the shutdown state and
 79      * the hook itself, but does not do any security checks.
 80      *
 81      * The registerShutdownInProgress parameter should be false except
 82      * registering the DeleteOnExitHook since the first file may
 83      * be added to the delete on exit list by the application shutdown
 84      * hooks.
 85      *
 86      * @params slot  the slot in the shutdown hook array, whose element
 87      *               will be invoked in order during shutdown
 88      * @params registerShutdownInProgress true to allow the hook
 89      *               to be registered even if the shutdown is in progress.
 90      * @params hook  the hook to be registered
 91      *
 92      * @throws IllegalStateException
 93      *         if registerShutdownInProgress is false and shutdown is in progress; or
 94      *         if registerShutdownInProgress is true and the shutdown process
 95      *         already passes the given slot
 96      */
 97     static void add(int slot, boolean registerShutdownInProgress, Runnable hook) {
 98         if (slot < 0 || slot >= MAX_SYSTEM_HOOKS) {
 99             throw new IllegalArgumentException("Invalid slot: " + slot);
100         }
101         synchronized (lock) {
102             if (hooks[slot] != null)
103                 throw new InternalError("Shutdown hook at slot " + slot + " already registered");
104 
105             if (!registerShutdownInProgress) {
106                 if (currentRunningHook >= 0)
107                     throw new IllegalStateException("Shutdown in progress");
108             } else {
109                 if (VM.isShutdown() || slot <= currentRunningHook)
110                     throw new IllegalStateException("Shutdown in progress");
111             }
112 
113             hooks[slot] = hook;
114         }
115     }
116 
117 //执行真正的shutdown钩子
118 /* Run all system shutdown hooks.
119      *
120      * The system shutdown hooks are run in the thread synchronized on
121      * Shutdown.class.  Other threads calling Runtime::exit, Runtime::halt
122      * or JNI DestroyJavaVM will block indefinitely.
123      *
124      * ApplicationShutdownHooks is registered as one single hook that starts
125      * all application shutdown hooks and waits until they finish.
126      */
127     private static void runHooks() {
128         synchronized (lock) {
129             /* Guard against the possibility of a daemon thread invoking exit
130              * after DestroyJavaVM initiates the shutdown sequence
131              */
132             if (VM.isShutdown()) return;
133         }
134 
135         for (int i=0; i < MAX_SYSTEM_HOOKS; i++) {
136             try {
137                 Runnable hook;
138                 synchronized (lock) {
139                     // acquire the lock to make sure the hook registered during
140                     // shutdown is visible here.
141                     currentRunningHook = i;
142                     hook = hooks[i];
143                 }
144                 if (hook != null) hook.run();
145             } catch (Throwable t) {
146                 if (t instanceof ThreadDeath) {
147                     ThreadDeath td = (ThreadDeath)t;
148                     throw td;
149                 }
150             }
151         }
152 
153         // set shutdown state
154         VM.shutdown();
155     }
156 
157 
158 
159 //这个Shutdown.runHooks何时执行?
160 /* Invoked by Runtime.exit, which does all the security checks.
161      * Also invoked by handlers for system-provided termination events,
162      * which should pass a nonzero status code.
163      */
164     static void exit(int status) {
165         synchronized (lock) {
166             if (status != 0 && VM.isShutdown()) {
167                 /* Halt immediately on nonzero status */
168                 halt(status);
169             }
170         }
171         synchronized (Shutdown.class) {
172             /* Synchronize on the class object, causing any other thread
173              * that attempts to initiate shutdown to stall indefinitely
174              */
175             beforeHalt();
176             runHooks();
177             halt(status);
178         }
179     }
180 
181 
182     /* Invoked by the JNI DestroyJavaVM procedure when the last non-daemon
183      * thread has finished.  Unlike the exit method, this method does not
184      * actually halt the VM.
185      */
186     static void shutdown() {
187         synchronized (Shutdown.class) {
188             runHooks();
189         }
190     }
191 
192 //再进一步,这个exit/shutdown方法什么时候执行
193 //exit方法再往上层追溯找到了我门熟悉的System.exit方法,shutdown暂时没找到调用之处
194 
195 //由此可见在执行System.exit方法之前会执行hooks里面所有的钩子

shutdownHook应用场景

很多时候,我们会有这样的一些场景,比如说nginx反向代理若干个负载均衡的web容器,又或者微服务架构中存在的若干个服务节点,需要进行无间断的升级发布。
在重启服务的时候,除非我们去变更nginx的配置,否则重启很可能会导致正在执行的线程突然中断,本来应该要完成的事情只完成了一半,并且调用方出现错误警告。
如果能有一种简单的方式,能够让进程在退出时能执行完当前正在执行的任务,并且让服务的调用方将新的请求定向到其他负载节点,这将会很有意义。
自己注册ShutdownHook可以帮助我们实现java进程的平滑退出。
设计思路:

  1. 在服务启动时注册自己的ShutdownHook
  2. ShutdownHook在被运行时,首先不接收新的请求,或者告诉调用方重定向到其他节点
  3. 等待当前的执行线程运行完毕,如果指定时间后仍在运行,则强制退出
原文地址:https://www.cnblogs.com/alan0521/p/13173475.html