Java应用正确地使用shutdownhook

Java应用正确地使用shutdownhook

在线上Java程序中经常遇到进程程挂掉,一些状态没有正确的保存下来,这时候就需要在JVM关掉的时候执行一些清理现场的代码。JDK在1.3之后提供了Java Runtime.addShutdownHook(Thread hook)方法,可以注册一个JVM关闭的钩子,这个钩子可以在以下几种场景被调用:

  1. 程序正常退出
  2. 使用System.exit()
  3. 终端使用Ctrl+C触发的中断
  4. 系统关闭
  5. 使用Kill pid命令干掉进程

注:在使用kill -9 pid 时JVM注册的钩子不会被调用。

在JDK中方法的声明:

1
2
3
4
5
6
7
public void addShutdownHook(Thread hook)
参数
hook -- 一个初始化但尚未启动的线程对象,注册到JVM钩子的运行代码。
异常
IllegalArgumentException -- 如果指定的钩已被注册,或如果它可以判定钩已经运行或已被运行
IllegalStateException -- 如果虚拟机已经是在关闭的过程中
SecurityException -- 如果存在安全管理器并且它拒绝的RuntimePermission(“shutdownHooks”)

标准中断信号

在Linux信号机制中,存在多种进程中断信号(Linux信号列表 )。其中比较典型的有 SIGNKILL(9) 和 SIGNTERM(15).

SIGNKILL(9) 和 SIGNTERM(15) 的区别在于:
SIGNKILL(9) 的效果是立即杀死进程. 该信号不能被阻塞, 处理和忽略。
SIGNTERM(15) 的效果是正常退出进程,退出前可以被阻塞或回调处理。并且它是Linux缺省的程序中断信号。

测试用例

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
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.atomic.AtomicInteger;

public class TestShutdownHook {

static Timer timer = new Timer("job-timer");
static AtomicInteger count = new AtomicInteger(0);

/**
* hook线程
*/
static class CleanWorkThread extends Thread{
@Override
public void run() {
System.out.println("clean some work.");
timer.cancel();
try {
Thread.sleep(2 * 1000);//sleep 2s
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
//将hook线程添加到运行时环境中去
Runtime.getRuntime().addShutdownHook(new CleanWorkThread());
System.out.println("main class start ..... ");
//简单模拟
timer.schedule(new TimerTask() {
@Override
public void run() {
count.getAndIncrement();
System.out.println("doing job " + count);
if (count.get() == 10) { //干了10次退出
System.exit(0);
}
}
}, 0, 2 * 1000);
}
}

shutdownhook的源码分析

ApplicationShutdownHooks

Runtime类中对ShutdownHook的操作都是通过工具类ApplicationShutdownHooks来实现的。

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
class ApplicationShutdownHooks {
// 利用Hash表来实现Map接口,比较键(和值)时使用引用相等性代替对象相等性
// 也就是说使用 == 而不是使用 equals
/* The set of registered hooks */
private static IdentityHashMap<Thread, Thread> hooks;
static {
try {
// 注册根shutdown(shutdown 槽)
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() {}

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);
}

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;
}

/* 遍历所有 application hooks 并启动线程*/
static void runHooks() {
Collection<Thread> threads;
synchronized(ApplicationShutdownHooks.class) {
threads = hooks.keySet();
hooks = null;
}

for (Thread hook : threads) {
hook.start(); // 此处说明shutdown hook 是乱序执行的
}
for (Thread hook : threads) {
while (true) {
try {
hook.join(); // hook 线程阻塞当前线程
break;
} catch (InterruptedException ignored) {
}
}
}
}
}

Shutdown

下面来看下Shutdown的源码

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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
class Shutdown {
// Shutdown 状态
private static final int RUNNING = 0;
private static final int HOOKS = 1;
private static final int FINALIZERS = 2;
private static int state = RUNNING;

// 是否退出时执行所有回收器
private static boolean runFinalizersOnExit = false;

// shutdown hook的 预注册 逻辑:
// (0) Console restore hook :处理命令行
// (1) Application hooks
// (2) DeleteOnExit hook
private static final int MAX_SYSTEM_HOOKS = 10; // 最大hook slot 数是10
private static final Runnable[] hooks = new Runnable[MAX_SYSTEM_HOOKS];

// 当前运行的hook在数组中的位置
private static int currentRunningHook = 0;

// 保护 static 字段的锁类
private static class Lock { };
// 锁对象
private static Object lock = new Lock();

/* 类锁的key,用于保证halt方法的并发 */
private static Object haltLock = new Lock();

/* 该方法被 Runtime.runFinalizersOnExit() 调用 */
static void setRunFinalizersOnExit(boolean run) {
synchronized (lock) {
runFinalizersOnExit = run;
}
}

/**
* 添加新的shutdown,检查状态和配置
*
* 除了注册DeleteOnExitHook的时侯,registerShutdownInProgress 参数默认设为false
* application shutdown hooks 在运行中的时候,可能会向DeleteOnExitHook添加文件
* File.deleteOnExit() 会调用 DeleteOnExitHook.add(path);
*
* @params slot 退出钩子的索引位置, 在shutdown期间按顺序执行
* @params registerShutdownInProgress true to allow the hook
* to be registered even if the shutdown is in progress.
* @params hook the hook to be registered
*
* @throw IllegalStateException
* if registerShutdownInProgress is false and shutdown is in progress; or
* if registerShutdownInProgress is true and the shutdown process
* already passes the given slot
*/
static void add(int slot, boolean registerShutdownInProgress, Runnable hook) {
synchronized (lock) {
if (hooks[slot] != null)
throw new InternalError("Shutdown hook at slot " + slot + " already registered");

if (!registerShutdownInProgress) {
if (state > RUNNING)
throw new IllegalStateException("Shutdown in progress");
} else {
// 1. 已经执行完了hook 阶段
// 2. 还在处理hook阶段,但是已经处理完了这个slot
if (state > HOOKS || (state == HOOKS && slot <= currentRunningHook))
throw new IllegalStateException("Shutdown in progress");
}

hooks[slot] = hook;
}
}

/* 运行所有的shutdownhook
*/
private static void runHooks() {
for (int i=0; i < MAX_SYSTEM_HOOKS; i++) {
try {
Runnable hook;
synchronized (lock) {
// 确保数组的可见性
currentRunningHook = i;
hook = hooks[i];
}
if (hook != null) hook.run(); // 注意此处是顺序执行的
// 使用的是run,而不是start()
} catch(Throwable t) {
if (t instanceof ThreadDeath) {
ThreadDeath td = (ThreadDeath)t;
throw td;
}
}
}
}
// 强制结束,status是非零退出码
static void halt(int status) {
synchronized (haltLock) {
// 此处加类锁,是为了防止处理 delete-on-shutdown 文件列表时的并发问题
halt0(status);
}
}
static native void halt0(int status);

/* Wormhole for invoking java.lang.ref.Finalizer.runAllFinalizers */
private static native void runAllFinalizers();


/* 真正的shutdown逻辑
*
* If it weren't for runFinalizersOnExit, this would be simple -- we'd just
* run the hooks and then halt. Instead we need to keep track of whether
* we're running hooks or finalizers. In the latter case a finalizer could
* invoke exit(1) to cause immediate termination, while in the former case
* any further invocations of exit(n), for any n, simply stall. Note that
* if on-exit finalizers are enabled they're run iff the shutdown is
* initiated by an exit(0); they're never run on exit(n) for n != 0 or in
* response to SIGINT, SIGTERM, etc.
*/
private static void sequence() {
synchronized (lock) {
/* 防止守护线程,在jvm 执行shutdown后调用exit()
*/
if (state != HOOKS) return;
}
runHooks(); // 运行钩子方法
boolean rfoe;
synchronized (lock) {
state = FINALIZERS; // 此处修改了state
rfoe = runFinalizersOnExit;
}
if (rfoe) runAllFinalizers(); // 执行所有的 finalizer
}


/* 被Runtime.exit()调用, 或者被系统终止事件handler调用。
*/
static void exit(int status) {
boolean runMoreFinalizers = false;
synchronized (lock) {
if (status != 0) runFinalizersOnExit = false; // 不正常退出
switch (state) {
case RUNNING: /* Initiate shutdown */
state = HOOKS;
break;
case HOOKS: /* Stall and halt */
break;
case FINALIZERS:
if (status != 0) {
// 非0状态立即停止
halt(status);
} else {
// 兼容旧逻辑,执行finalizer后退出
runMoreFinalizers = runFinalizersOnExit;
}
break;
}
}
if (runMoreFinalizers) {
runAllFinalizers();
halt(status);
}
synchronized (Shutdown.class) {
/* Synchronize on the class object, causing any other thread
* that attempts to initiate shutdown to stall indefinitely
*/
sequence();
halt(status);
}
}


/* 通过JNI 调用
* Invoked by the JNI DestroyJavaVM procedure when the last non-daemon
* thread has finished. Unlike the exit method, this method does not
* actually halt the VM.
*/
static void shutdown() {
synchronized (lock) {
switch (state) {
case RUNNING: /* Initiate shutdown */
state = HOOKS;
break;
case HOOKS: /* Stall and then return */
case FINALIZERS:
break;
}
}
synchronized (Shutdown.class) {
sequence();
}
}
}

shutdown的字段state的状态机如下图所示:

            graph LR
            id0((begin))-->id1(running)
id1-->|"Runtime.exit()"|id2(HOOKS)
id1-->|"shutdown()"|id2
id2-->|"sequence()"|id3(FINALIZERS)
id3-->id4((end))
          

delete-on-exit file list

java.io.File的deleteOnExit()方法可以在JVM退出时,删除文件。主要的实现就是DeleteOnExitHook.

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
class DeleteOnExitHook {
private static LinkedHashSet<String> files = new LinkedHashSet<>();
static {
// DeleteOnExitHook 必须是最后一个被调用的shutdown hook。
// Application shutdown hooks 可能会向 delete on exit file list中添加文件,
// 此时shutdown的状态是 hooks, 所以将 registerShutdownInProgress 设置为 true.
sun.misc.SharedSecrets.getJavaLangAccess()
.registerShutdownHook(2 /* Shutdown hook invocation order */,
true /* register even if shutdown in progress */,
new Runnable() {
public void run() {
runHooks();
}
}
);
}

private DeleteOnExitHook() {}

static synchronized void add(String file) {
if(files == null) {
// DeleteOnExitHook is running. Too late to add a file
throw new IllegalStateException("Shutdown in progress");
}

files.add(file);
}

static void runHooks() {
LinkedHashSet<String> theFiles;

synchronized (DeleteOnExitHook.class) {
theFiles = files;
files = null;
}

ArrayList<String> toBeDeleted = new ArrayList<>(theFiles);

// reverse the list to maintain previous jdk deletion order.
// Last in first deleted.
Collections.reverse(toBeDeleted);
for (String filename : toBeDeleted) {
(new File(filename)).delete();
}
}
}
-------------本文结束感谢您的阅读-------------
坚持分享,您的支持将鼓励我继续创作!
0%