最近碰到一個(gè)問(wèn)題假夺,通過(guò)腳本執(zhí)行kill -15
后淮蜈,程序并沒(méi)有退出,進(jìn)程一直都在已卷,最后被退出腳本的通過(guò)kill -9
梧田,殺死。導(dǎo)致數(shù)據(jù)完整性被破壞悼尾,程序再重啟后不可用柿扣。通過(guò)排查認(rèn)后發(fā)現(xiàn)是在執(zhí)行shutdownHook
時(shí)死鎖程序死鎖。
復(fù)現(xiàn)問(wèn)題
導(dǎo)致問(wèn)題的代碼闺魏,
通過(guò)定位發(fā)現(xiàn)未状,程序在
public class Test {
private static final Object lock = new Object();
public static void main(String... args) {
Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Locking");
synchronized (lock) {
System.out.println("Locked");
}
}
}));
synchronized (lock) {
System.out.println("Exiting");
System.exit(0);
}
}
}
輸出:
Exiting
Locking
原因
排查原因
分析一下 addShutdownHook 這個(gè)方法是怎么執(zhí)行的,重點(diǎn)是 ApplicationShutdownHooks析桥,每一個(gè) shutdownHook 都使用一個(gè)Thread包裝司草。
public void addShutdownHook(Thread hook) {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(new RuntimePermission("shutdownHooks"));
}
ApplicationShutdownHooks.add(hook);
}
重點(diǎn):hooks艰垂,每個(gè) hook線程put到hooks中。
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);
}
添加后誰(shuí)來(lái)處理shutdown這個(gè)操作埋虹,是 Shutdown.add 這里起了一個(gè)線程猜憎,處理所以主要的邏輯在 runHooks
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;
}
}
這段代碼中 hook.start(); 調(diào)用執(zhí)行 hook的方法,之后調(diào)用 hook.join釋放執(zhí)行權(quán)搔课。
問(wèn)題就出在 hook.join上胰柑,程序執(zhí)行到這里之后,卡住死鎖爬泥,出不去了柬讨。
為什么,因?yàn)?join 實(shí)際就是 wait(0)袍啡,一旦當(dāng)前線程調(diào)用wait(0)踩官,就相當(dāng)于釋放執(zhí)行權(quán),等待其實(shí)線程notify()才能繼續(xù)執(zhí)行境输。
但是main線程調(diào)用System.exit(0)后蔗牡,synchronized 當(dāng)前線程為 main,hook.join拿不到被main未釋放的鎖嗅剖,所以卡住
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) {
}
}
}
}
通過(guò)工具排查
再看線程狀態(tài)
通過(guò)代碼線程堆棧來(lái)確認(rèn)就是這個(gè)原因
- main 方法是:WAIT 狀態(tài)
- Thread-0是:RUNNING 狀態(tài)辩越,但是進(jìn)入synchronized之后就會(huì)BLOCKED住
這里就對(duì)應(yīng)上圖的兩個(gè)線程的狀態(tài)
解決
移除 shutdownHook 中不必要的加鎖。
- 移除 shutdownHook 中不必要的加鎖信粮,shutdown 場(chǎng)景中很不需要用到加鎖
- 使用不同的加鎖對(duì)象区匣,如果一定需要加鎖,可以在 shutdownHook 的線程內(nèi)使用一把新的鎖蒋院,這樣即可以保證安全性亏钩,又不會(huì)死鎖。