Java关闭钩子 - Shutdown Hook
背景
在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关闭的钩子,这个钩子可以在一下几种场景中被调用:
- 程序正常退出
- 使用System.exit()
- 终端使用Ctrl+C触发的中断
- 系统关闭
- OutOfMemory宕机
- 使用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 }
源码解读
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进程的平滑退出。
设计思路:
- 在服务启动时注册自己的ShutdownHook
- ShutdownHook在被运行时,首先不接收新的请求,或者告诉调用方重定向到其他节点
- 等待当前的执行线程运行完毕,如果指定时间后仍在运行,则强制退出