死锁问题与解决方案
总结
- 死锁需要同时满足四个条件,破坏任意一个就能避免
- 最推荐:按固定顺序加锁(破坏循环等待),简单有效
- 线上推荐用
tryLock+ 超时,比synchronized更安全 - 数据库死锁 DB 会自动回滚,但操作行要按固定顺序
1. 先说结论
死锁需要同时满足四个条件,破坏任意一个就能避免。实际开发中最常用的是破坏"循环等待",按固定顺序加锁,简单有效。
2. 四个必要条件
| 条件 | 能否破坏 | 方法 |
|---|---|---|
| 互斥 | ❌ | 锁的本质,没法破坏 |
| 占用且等待 | ✅ | 一次性申请所有资源 |
| 不可抢占 | ✅ | 申请不到就主动释放已持有的锁 |
| 循环等待 | ✅ | 按固定顺序申请锁(最推荐) |
3. 怎么破坏?
3.1 破坏循环等待(推荐)
按资源 ID 排序,所有线程都按同一顺序加锁,就不会形成环:
void transfer(Account target, int amt) {
// 按 ID 大小决定加锁顺序,所有线程都一样
Account first = this.id < target.id ? this : target;
Account second = this.id < target.id ? target : this;
synchronized (first) {
synchronized (second) {
if (this.balance > amt) {
this.balance -= amt;
target.balance += amt;
}
}
}
}
3.2 破坏不可抢占
用 tryLock 替代 synchronized,拿不到锁就主动放弃已持有的,随机等一会再重试:
void transfer(Account target, int amt) throws InterruptedException {
while (true) {
if (this.lock.tryLock()) {
try {
if (target.lock.tryLock()) {
try {
if (this.balance > amt) {
this.balance -= amt;
target.balance += amt;
}
return;
} finally {
target.lock.unlock();
}
}
} finally {
this.lock.unlock(); // 拿不到 target 锁,释放 this 锁
}
}
Thread.sleep((long) (Math.random() * 10)); // 随机退避,避免活锁
}
}
3.3 破坏占用且等待
用一个 Allocator 统一管理资源,要么一次拿到所有锁,要么一个都不拿:
class Allocator {
private List<Object> resources = new ArrayList<>();
synchronized boolean apply(Object from, Object to) {
if (resources.contains(from) || resources.contains(to)) {
return false;
}
resources.add(from);
resources.add(to);
return true;
}
synchronized void free(Object from, Object to) {
resources.remove(from);
resources.remove(to);
}
}
void transfer(Account target, int amt) {
while (!allocator.apply(this, target)) { /* 等待 */ }
try {
synchronized (this) {
synchronized (target) {
if (this.balance > amt) {
this.balance -= amt;
target.balance += amt;
}
}
}
} finally {
allocator.free(this, target);
}
}
4. 死锁检测
线上排查用 ThreadMXBean:
ThreadMXBean bean = ManagementFactory.getThreadMXBean();
long[] deadlocked = bean.findDeadlockedThreads();
if (deadlocked != null) {
for (ThreadInfo info : bean.getThreadInfo(deadlocked)) {
System.out.println("死锁线程: " + info.getThreadName());
System.out.println("等待的锁: " + info.getLockName());
}
}
5. 几个注意点
相关学习路径:原子性解决方案-互斥锁 → 等待-通知机制 → AQS抽象队列同步器原理
synchronized拿不到锁会一直等,线上更推荐用tryLock+ 超时- 数据库死锁不用手动处理,DB 会自动检测并回滚其中一个事务,但要注意按固定顺序操作行
- 嵌套锁越少越好,能用一把锁解决的别用两把