一个有效的收拾程序运行残局的方法--ShutdownHook

发现addShutdownHook

在阅读QMQ的源码的时候,在Server端启动的时候,注册了一个shutdown的代码,具体的代码如下:

1
2
Runtime.getRuntime().addShutdownHook(new Thread(wrapper::destroy));

addShutdownHook作用

addShutdownHook方法可以添加一个指定的线程来在Java程序退出的时候做一些事情,在以下几个场景会被调用:

  1. 程序运行完成后退出
  2. 使用Ctrl+C时终端退出
  3. 调用系统退出的方法, System.exit(0)

具体的使用Demo如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class AppMain {
public static void main(String[] args) {
Runtime.getRuntime().addShutdownHook(new Thread(AppMain::exit01));
Runtime.getRuntime().addShutdownHook(new Thread(AppMain::exit02));
Runtime.getRuntime().addShutdownHook(new Thread(AppMain::exit03));
while (true){
System.out.println(".....");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}

}
public static void exit01(){
System.out.println("exit01");
}
public static void exit02(){
System.out.println("exit02");
}
public static void exit03(){
System.out.println("exit03");
}
}

执行当执行退出的时候:
程序退出

addShutdownHook的使用场景

addShutdownHook的使用场景很多, 尤其是在通信的模块功能,比如Netty为例,当服务端或者客户端异常断开的时候,需要告诉对方释放资源,从而保证不会触发异常机制。

例如分布式队列,RPC框架都需要在异常后,释放相关资源。

addShutdownHook使用注意几点

  1. 在多次使用addShutdownHook增加退出的方法是,由于是开一个线程,所以不能保证方法的执行是按顺序的。 所以尽量保证方法在一个方法里面完成。

  2. 在释放的时候不应该增加阻塞的方法,否者会导致推出时阻塞,导致无法退出

    1
    2
    3
    4
    5
    Runtime.getRuntime().addShutdownHook(new Thread(() -> {
    while (true) {
    //
    }
    }));

hooks相关源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void addShutdownHook(Thread hook) {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(new RuntimePermission("shutdownHooks"));
}
ApplicationShutdownHooks.add(hook);
}

public boolean removeShutdownHook(Thread hook) {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(new RuntimePermission("shutdownHooks"));
}
return ApplicationShutdownHooks.remove(hook);
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81

class ApplicationShutdownHooks {
/* 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() {
runHooks();
}
}
);
hooks = new IdentityHashMap<>();
} catch (IllegalStateException e) {
// application shutdown hooks cannot be added if
// shutdown is in progress.
hooks = null;
}
}


private ApplicationShutdownHooks() {}

/* Add a new shutdown hook. Checks the shutdown state and the hook itself,
* but does not do any security checks.
*/
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);
}

/* Remove a previously-registered hook. Like the add method, this method
* does not do any security checks.
*/
static synchronized boolean remove(Thread hook) {
if(hooks == null)
throw new IllegalStateException("Shutdown in progress");

if (hook == null)
throw new NullPointerException();

return hooks.remove(hook) != null;
}

/* 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) {
while (true) {
try {
hook.join();
break;
} catch (InterruptedException ignored) {
}
}
}
}
}


Hooks管理问题

在很多开源的框架中,有的Hooks反而会起到反作用,我们怎么样能够移除我们不需要的Hooks呢?

在看了上面的代码,我们发现ApplicationShutdownHooks 不是一个public的类,JDK也没有提供对外管理的方法,只有addremove方法。

通过分析ApplicationShutdownHooks类源码的结构,我们可以发现,用来存放hooks的文件的是一个IdentityHashMap容器,而且是一个静态的属性。

从反射入手

既然是一个变量,可以通过反射来获得对应的Hooks的值,代码如下:

1
2
3
4
5
6
7
8
String className = "java.lang.ApplicationShutdownHooks";
Class<?> clazz = Class.forName(className);
Field field = clazz.getDeclaredField("hooks");
field.setAccessible(true);
IdentityHashMap<Thread, Thread> map = (IdentityHashMap<Thread, Thread>) field.get(clazz);
for (Thread thread : map.keySet()) {
RzLogger.info("found shutdownHook: " + thread.getName());
}

增加三个Hook验证:

1
2
3
4
5
6

Runtime.getRuntime().addShutdownHook(new Thread(AppMain::exit01));
Runtime.getRuntime().addShutdownHook(new Thread(AppMain::exit02));
Runtime.getRuntime().addShutdownHook(new Thread(AppMain::exit03));


1
2
3
4
5
6
7
8

23:33:11,102 INFO [main] (RzLogger.java:12) - found shutdownHook: Thread-2
23:33:11,106 INFO [main] (RzLogger.java:12) - found shutdownHook: Thread-0
23:33:11,106 INFO [main] (RzLogger.java:12) - found shutdownHook: Thread-1
exit03
exit01
exit02

收获

  1. ShutDown的使用,在程序退出后清理现场
  2. 一步一步去理解代码的原理和特性,细节很重要