最近碰到一个问题,通过脚本执行kill -15
后,程序并没有退出,进程一直都在,最后被退出脚本的通过kill -9
,杀死。导致数据完整性被破坏,程序再重启后不可用。通过排查认后发现是在执行shutdownHook
时死锁程序死锁。
复现问题
导致问题的代码,通过定位发现,程序在退出时卡住,线上代码敏感,写一个demo来复现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| 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 这个方法是怎么执行的,重点是 ApplicationShutdownHooks,每一个 shutdownHook 都使用一个Thread包装。
1 2 3 4 5 6 7
| public void addShutdownHook(Thread hook) { SecurityManager sm = System.getSecurityManager(); if (sm != null) { sm.checkPermission(new RuntimePermission("shutdownHooks")); } ApplicationShutdownHooks.add(hook); }
|
重点:hooks,每个 hook线程put到hooks中。
1 2 3 4 5 6 7 8 9 10 11 12
| 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); }
|
添加后谁来处理shutdown这个操作,是 Shutdown.add 这里起了一个线程,处理所以主要的逻辑在 runHooks
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| static { try { Shutdown.add(1 , false , new Runnable() { public void run() { runHooks(); } } ); hooks = new IdentityHashMap<>(); } catch (IllegalStateException e) { hooks = null; } }
|
这段代码中 hook.start(); 调用执行 hook的方法,之后调用 hook.join释放执行权。
问题就出在 hook.join上,程序执行到这里之后,卡住死锁,出不去了。
为什么,因为 join 实际就是 wait(0),一旦当前线程调用wait(0),就相当于释放执行权,等待其实线程notify()才能继续执行。
但是main线程调用System.exit(0)后,synchronized 当前线程为 main,hook.join拿不到被main未释放的锁,所以卡住
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| 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) { } } } }
|
通过工具排查,可以清楚的看到,Thread-0 即shutdown线程去引用已经被main线程持有的锁对象,而导至被 BLOCKED 住
再看线程状态
通过代码线程堆栈来确认就是这个原因
- main 方法是:WAIT 状态
- Thread-0是:RUNNING 状态,但是进入synchronized之后就会BLOCKED住
这里就对应上图的两个线程的状态
解决
即然已经知道原因了,那就好办:
- 移除 shutdownHook 中不必要的加锁,shutdown 场景中很不需要用到加锁
- 使用不同的加锁对象,如果一定需要加锁,可以在 shutdownHook 的线程内使用一把新的锁,这样即可以保证安全性,又不会死锁。