在Java并发编程中,死锁就像两个固执的谈判代表,各自坚持自己的立场不肯让步,最终导致谈判陷入僵局。我曾在生产环境遇到过这样一个案例:支付系统在高峰期出现服务不可用,最终排查发现是两个微服务线程因数据库锁竞争形成了死锁链。
死锁的四个必要条件(Coffman条件)就像组成致命组合的四把钥匙:
互斥条件:资源如同独木桥,一次只能通过一个线程。比如synchronized修饰的方法或代码块,同一时刻仅允许一个线程进入。
占有且等待:线程像贪心的食客,左手拿着叉子不放,右手还想拿刀子。代码中表现为线程持有锁A的同时,又尝试获取锁B。
不可剥夺:Java中的锁就像粘在手上的胶水,除非线程主动释放(执行完同步块或调用unlock()),否则系统不能强行剥夺。
循环等待:多个线程形成等待环,就像一群人围成一圈,每个人都等着前面的人先行动。在代码中表现为Thread1等待Thread2持有的资源,同时Thread2又在等待Thread1的资源。
重要提示:这四个条件必须同时满足才会产生死锁,打破任意一个条件即可预防死锁。这也是我们设计并发程序时的突破口。
下面这个例子是我在面试候选人时必问的"送分题",但能完整解释清楚原理的不到30%:
java复制public class ClassicDeadlock {
private static final Object lock1 = new Object();
private static final Object lock2 = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (lock1) {
System.out.println("Thread1 acquired lock1");
try { Thread.sleep(50); }
catch (InterruptedException e) {}
synchronized (lock2) {
System.out.println("Thread1 acquired lock2");
}
}
}).start();
new Thread(() -> {
synchronized (lock2) {
System.out.println("Thread2 acquired lock2");
try { Thread.sleep(50); }
catch (InterruptedException e) {}
synchronized (lock1) {
System.out.println("Thread2 acquired lock1");
}
}
}).start();
}
}
这段代码的致命之处在于:
为什么说这个例子"必然"死锁?我们可以用线程执行时序图来说明:
code复制时间线 Thread1 Thread2
-----------------------------------------------------
t1 获取lock1 (成功)
t2 获取lock2 (成功)
t3 尝试获取lock2 (阻塞)
t4 尝试获取lock1 (阻塞)
通过jstack工具查看线程堆栈时,会看到类似这样的死锁报告:
code复制Found one Java-level deadlock:
=============================
"Thread-1":
waiting to lock monitor 0x00007f8934003f58 (object 0x000000076bf4c7d8, a java.lang.Object),
which is held by "Thread-0"
"Thread-0":
waiting to lock monitor 0x00007f89340041f8 (object 0x000000076bf4c7e8, a java.lang.Object),
which is held by "Thread-1"
bash复制# 先使用jps找到Java进程ID
jps -l
# 然后使用jstack分析
jstack <pid>
bash复制jconsole
在代码中可以通过ThreadMXBean动态检测死锁:
java复制public class DeadlockDetector {
public static void startDetection() {
ThreadMXBean mxBean = ManagementFactory.getThreadMXBean();
Runnable detector = () -> {
while (true) {
long[] deadlockedThreads = mxBean.findDeadlockedThreads();
if (deadlockedThreads != null) {
System.err.println("Deadlock detected!");
ThreadInfo[] threadInfos = mxBean.getThreadInfo(deadlockedThreads);
for (ThreadInfo info : threadInfos) {
System.err.println(info);
}
break;
}
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
};
new Thread(detector, "DeadlockDetector").start();
}
}
这是最有效的预防措施之一。在我们的支付系统中,制定了这样的规范:
java复制public class LockManager {
public static void lockInOrder(Object lock1, Object lock2, Runnable task) {
Object firstLock = System.identityHashCode(lock1) < System.identityHashCode(lock2) ? lock1 : lock2;
Object secondLock = firstLock == lock1 ? lock2 : lock1;
synchronized (firstLock) {
synchronized (secondLock) {
task.run();
}
}
}
}
使用ReentrantLock的tryLock方法可以有效避免无限等待:
java复制public class TransferService {
private final ReentrantLock fromLock = new ReentrantLock();
private final ReentrantLock toLock = new ReentrantLock();
public boolean transfer(Account from, Account to, BigDecimal amount) {
long timeout = 1000; // 1秒超时
try {
if (fromLock.tryLock(timeout, TimeUnit.MILLISECONDS)) {
try {
if (toLock.tryLock(timeout, TimeUnit.MILLISECONDS)) {
try {
// 执行转账逻辑
return true;
} finally {
toLock.unlock();
}
}
} finally {
fromLock.unlock();
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return false;
}
}
对于常见场景,优先使用Java并发包中的高级工具:
去年我们电商系统在双十一期间出现过一次严重的死锁问题,现象是订单服务间歇性无响应。通过分析线程dump,发现是以下调用链导致的:
code复制线程A:
1. 持有订单表的行锁(更新订单状态)
2. 尝试获取用户表的行锁(更新用户积分)
线程B:
1. 持有用户表的行锁(查询用户信息)
2. 尝试获取订单表的行锁(创建新订单)
解决方案:
活锁就像两个过于礼貌的人相遇在走廊:
典型场景:
java复制public class PoliteWorker {
private boolean sharedResource = false;
public void work() {
while (!sharedResource) {
if (checkIfOtherThreadNeeds()) {
Thread.yield(); // 过于礼貌的让步
continue;
}
sharedResource = true;
}
// 实际工作代码
}
}
解决方法:引入随机退避机制,避免完全对称的响应逻辑。
线程饥饿就像食堂排队时总是被插队的新同学:
解决方案:
根据我在多个高并发项目的经验,以下措施能有效降低死锁风险:
对于关键业务系统,我建议实现一个锁监控看板,实时展示:
这个案例让我深刻理解到,在并发编程中,预防胜于治疗。与其事后排查死锁,不如在代码设计阶段就规避风险。