前面用两个线程演示过死锁。这次把执行顺序固定下来,再用线程堆栈看一下它们到底在等什么。
构造互相等待
import java.util.concurrent.CountDownLatch;
public class DeadlockCheckDemo {
private static final Object FIRST = new Object();
private static final Object SECOND = new Object();
private static final CountDownLatch HELD = new CountDownLatch(2);
private static void acquire(Object own, Object other) {
synchronized (own) {
HELD.countDown();
try {
HELD.await();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
synchronized (other) {
System.out.println("取得两把锁");
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread a = new Thread(() -> acquire(FIRST, SECOND), "lock-a");
Thread b = new Thread(() -> acquire(SECOND, FIRST), "lock-b");
a.start();
b.start();
a.join();
b.join();
}
}CountDownLatch 在这里让两个线程先各自取得第一把锁,再尝试第二把。这个程序是故意写出的死锁示例,运行后不会自行结束,观察完成后需要结束进程。
lock-a 持有 FIRST 等 SECOND,lock-b 持有 SECOND 等 FIRST。它们都不会先退出外层同步块,因此谁也无法继续。
死锁通常涉及四个条件:资源互斥、持有资源继续等待、资源不能被强行夺走、等待关系形成环路。这里四项都具备。
查看线程堆栈
在另一个终端找到 Java 进程,再导出堆栈:
jps -l
jstack <pid>重点看 lock-a、lock-b 两个线程的栈,找到各自的 locked 和 waiting to lock。如果一个线程等待的对象,正好出现在另一个线程已经持有的锁里,再反过来对上,就能看到完整的等待环。
jstack 还会报告检测到的 Java 级死锁。也可以用 jconsole 连接这个本地进程,在“线程”页检测死锁。对象地址每次运行可能不同,不能拿某次输出中的地址当固定标识。
打破等待环
最直接的修改,是让两个线程始终按 FIRST、SECOND 的顺序获取锁。注意还要删除示例中的 HELD 等待:如果第一个线程持有 FIRST 后等第二个线程到达,而第二个线程又拿不到 FIRST,仍然会卡住。
多个业务对象需要一起加锁时,可以按一个稳定且唯一的编号排序。不能直接拿可能相同的哈希值当成绝对顺序,除非相等时也有单独的处理规则。
另一种方式是用 ReentrantLock.tryLock() 设置等待上限。第二把锁获取失败,就释放已经拿到的第一把锁,再决定是否重试。双方完全同步地反复退让也可能形成活锁,所以超时只提供退出机会,并不是自动解决了所有协调问题。
排查时还要注意:长时间等待不一定是死锁。网络调用迟迟不返回、线程池没有空闲线程,也可能表现为“程序不动了”。先看线程栈和等待关系,再判断原因。