一文搞懂 Java 死锁:四大必要条件、经典死锁 Demo、如何用代码和思想避免死锁
死锁(Deadlock)是 Java 并发中最容易踩坑,也最容易被忽视的问题。
它不是异常,不会报错,但会让线程永久卡住,系统彻底失去响应。
这一篇文章,从 死锁原理 → 复现 → 避免策略 做一个完整讲解。
1. 什么是死锁?一句话概念
死锁 = 多个线程互相等待对方持有的资源(锁),形成循环等待,导致程序永久卡死。
示意:
T1 持有锁 A,等待锁 B
T2 持有锁 B,等待锁 A
双方互相等待 → 永久挂死
2. 死锁的“四大必要条件”(必须背)
只要这四个条件同时满足,必死锁:
互斥:资源(如锁)一次只能被一个线程占有
占有并等待:线程已获取锁 A,还要等待锁 B
不可抢占:锁不能被强制夺走
循环等待:多个线程之间互相等待形成环路
只要破坏任意一个条件,死锁就不会发生。
3. 最经典的 Java 死锁 Demo(必看)
public class DeadlockDemo {
private static final Object LOCK_A = new Object();
private static final Object LOCK_B = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (LOCK_A) {
System.out.println("T1 拿到 A");
try { Thread.sleep(100); } catch (Exception ignored) {}
System.out.println("T1 想要 B...");
synchronized (LOCK_B) {
System.out.println("T1 拿到 B");
}
}
});
Thread t2 = new Thread(() -> {
synchronized (LOCK_B) {
System.out.println("T2 拿到 B");
try { Thread.sleep(100); } catch (Exception ignored) {}
System.out.println("T2 想要 A...");
synchronized (LOCK_A) {
System.out.println("T2 拿到 A");
}
}
});
t1.start();
t2.start();
}
}
运行结果:
T1 拿到 A
T2 拿到 B
T1 想要 B...
T2 想要 A...
(程序卡住不动)
4. 死锁是怎么形成的?(图解一次性看懂)
Thread T1: 拿到锁 A → 想要锁 B(被 T2 占有)
Thread T2: 拿到锁 B → 想要锁 A(被 T1 占有)
形成循环等待:
T1 → 等待 B → 被 T2 持有
T2 → 等待 A → 被 T1 持有
这时:
谁都不能继续
谁都不会释放锁
程序永久卡死
5. ReentrantLock 一样能死锁(换锁不解决死锁问题)
把 synchronized 换成 ReentrantLock,也会死锁:
ReentrantLock lockA = new ReentrantLock();
ReentrantLock lockB = new ReentrantLock();
Thread t1 = new Thread(() -> {
lockA.lock();
try {
Thread.sleep(100);
lockB.lock(); // 可能死锁
try { } finally { lockB.unlock(); }
} catch (Exception ignored) {
} finally {
lockA.unlock();
}
});
死锁不是 synchronized 的缺陷,也不是 ReentrantLock 的缺陷。
死锁来源于“加锁顺序不一致”。
6. 用 ReentrantLock 的 tryLock 避免死锁(极其有效)
synchronized 不支持“尝试加锁”,只能一直等。ReentrantLock 支持 tryLock(),拿不到锁可以直接放弃,避免无限等待。
示例:
if (lockA.tryLock(500, TimeUnit.MILLISECONDS)) {
try {
if (lockB.tryLock(500, TimeUnit.MILLISECONDS)) {
try {
// 成功获取两把锁
} finally {
lockB.unlock();
}
} else {
System.out.println("获取 B 超时,避免死锁");
}
} finally {
lockA.unlock();
}
}
只要在超时时间内做“放弃操作”,就能有效破坏循环等待 → 避免死锁。
7. 实战中避免死锁的 5 条原则(必须记住)
✔ 1. 所有线程必须使用一致的加锁顺序(最重要)
例如规定:
所有地方 → 必须先锁 A,再锁 B
就不会发生:
线程1:A → B
线程2:B → A
✔ 2. 尽量减少锁的数量
锁越多,越容易死锁
尽量减少嵌套锁
✔ 3. 不要在锁内做耗时操作
包括:
sleep
I/O
数据库操作
网络调用
耗时越长,被其他线程等待的时间越长,死锁几率越高。
✔ 4. 多用 ReentrantLock 的 tryLock + 超时机制
逻辑思路是:
拿不到锁 → 马上放弃
不形成无限等待
这可以破坏死锁的关键条件“循环等待”。
✔ 5. 不要混用不同的锁去保护同一资源
例如:
一会儿 synchronized(this)
一会儿 lock.lock()
这种会导致不可控锁顺序,风险极大。
8. 总结:一篇搞懂死锁所有关键点
最后把重点再浓缩成简洁版,方便你记忆:
✦ 死锁是什么?
线程之间互相等待,形成循环依赖,程序永久卡死。
✦ 死锁的四大必要条件
互斥
占有并等待
不可抢占
循环等待
✦ synchronized 会死锁吗?
会。
ReentrantLock 会死锁吗?
也会。
因为死锁是锁顺序问题,不是锁类型问题。
✦ 怎么避免死锁?
统一加锁顺序(最有效)
减少锁数量
锁内不要做耗时任务
使用 tryLock 超时机制
避免混用锁
写在最后
理解死锁,不是为了知道“它怎么发生”,
而是为了在设计系统时主动避免死锁。
“可重入锁解决自己锁自己,死锁解决互相等待。
死锁的根源不是 synchronized,而是程序设计。”