理解死锁:场景、实例与预防策略
什么是死锁
死锁(Deadlock) 是指两个或多个线程在运行过程中,由于争夺资源而造成一种相互等待的现象,导致它们都无法继续执行。
死锁通常发生在多线程程序中,当多个线程都试图获取对方持有的资源时,就可能陷入“你等我,我等你”的状态,程序因此进入僵局。
在 Java 中,死锁多见于同时持有多把锁的场景,例如使用 synchronized
或 ReentrantLock
时多个线程互相抢占资源。
死锁实例代码
public class DeadlockDemo {private final Object lock1 = new Object();private final Object lock2 = new Object();public void thread1() {synchronized (lock1) {System.out.println("Thread1: 持有lock1");try { Thread.sleep(50); } catch (InterruptedException e) {}synchronized (lock2) {System.out.println("Thread1: 持有lock1和lock2");}}}public void thread2() {synchronized (lock2) {System.out.println("Thread2: 持有lock2");try { Thread.sleep(50); } catch (InterruptedException e) {}synchronized (lock1) {System.out.println("Thread2: 持有lock2和lock1");}}}public static void main(String[] args) {DeadlockDemo demo = new DeadlockDemo();new Thread(demo::thread1).start();new Thread(demo::thread2).start();}
}两个线程互相等待对方的锁,导致程序死锁。
死锁产生的四个必要条件
死锁(Deadlock)是一种 多个线程或进程互相占有资源、相互等待,导致都无法继续运行的状态。
它必须同时满足这4个条件:
1. 互斥条件(Mutual Exclusion)
-
解释:
- 某个资源在同一时间只能被一个线程占用。
- 其他线程只能等待该资源被释放后才能访问。
- 例如:两个线程都想打印日志,但打印机只有一个。
-
Java 中的体现:
- synchronized 块、ReentrantLock、文件句柄等都是互斥资源。
-
打破策略:
- 共享资源:如果可以让多个线程“共享读”,可以用 读写锁(
ReentrantReadWriteLock
),允许多个线程同时读。 - 无锁化:用 CAS (Compare-And-Swap) 这种原子操作替代锁,比如 Java 的
AtomicInteger
。 - 乐观并发:像数据库的乐观锁一样,认为冲突很少发生,先不加锁,冲突时再回滚。
- 共享资源:如果可以让多个线程“共享读”,可以用 读写锁(
2. 请求与保持条件(Request and Maintain)
-
解释:
- 线程 已经持有一个资源,但它还想申请其他资源,而且在申请新资源的同时不释放已持有的资源。
- 例如:线程 A 拿着资源 X,要资源 Y;线程 B 拿着资源 Y,要资源 X。
-
Java 中的体现:
-
线程拿到一把锁后继续申请另一把锁,比如:
synchronized (lock1) {synchronized (lock2) {// ...} }
-
-
打破策略:
-
一次性申请所有资源:
让线程在执行前先申请所需的所有锁,如果申请失败就释放已拿到的资源,稍后重试。 -
使用 tryLock + 超时策略:
if (lock1.tryLock(1, TimeUnit.SECONDS)) {try {if (lock2.tryLock(1, TimeUnit.SECONDS)) {try {// 临界区} finally {lock2.unlock();}} else {// 获取 lock2 失败,释放 lock1}} finally {lock1.unlock();} }
不等待另一把锁,避免死等。
-
3. 不可剥夺条件(No Preemption)
-
解释:
- 线程 已获得的资源,在没使用完之前,不能被系统强制剥夺,只能自己释放。
- 例如:一个线程占用数据库连接,其他线程只能等它手动释放。
-
Java 中的体现:
- synchronized 和普通 ReentrantLock 都是不可剥夺的。
- 一旦线程拿到锁,其他线程只能一直等。
-
打破策略:
-
使用可中断锁:
用lockInterruptibly()
,如果线程被中断,可以及时退出等待。lock.lockInterruptibly();
-
超时锁:
用tryLock(timeout)
,避免无限等待。 -
强制回滚(在事务/数据库层实现):如果一个线程长期占用资源,其他线程可以中断它,让其释放资源。
-
4. 循环等待条件(Circular Wait)
-
解释:
-
存在一个线程等待环,每个线程都在等待下一个线程释放资源。
-
例如:
T1: 持有 lockA,等待 lockB T2: 持有 lockB,等待 lockA
-
-
Java 中的体现:
- 多线程同时请求多把锁,但获取锁的顺序不同。
-
打破策略:
-
规定固定的锁获取顺序:
所有线程必须按照统一顺序申请锁,例如根据对象 ID 排序:Object lockA, lockB;Object first = lockA.hashCode() < lockB.hashCode() ? lockA : lockB; Object second = lockA.hashCode() < lockB.hashCode() ? lockB : lockA;synchronized (first) {synchronized (second) {// 临界区} }
这样避免了 T1、T2 拿锁顺序不一致导致的环。
-
资源分级法:
给资源编号,只允许线程按编号递增顺序申请。
-
如何打破死锁的四个必要条件
要避免死锁,必须破坏至少一个必要条件,具体方法如下:
条件 | 解释 | 打破方法 |
---|---|---|
互斥 | 资源一次只能被一个线程占用 | 使用无锁算法(如 CAS)、读写锁(允许多线程共享读) |
占有且等待 | 已占有资源的线程等待其他资源时不释放已有资源 | 一次申请所有资源;使用 tryLock 获取失败时释放已有资源并重试 |
不可剥夺 | 已获得的资源不能被强制剥夺 | 使用 lockInterruptibly() 或 tryLock(timeout) ,支持超时退出和中断 |
循环等待 | 存在线程资源等待环 | 规定统一的资源申请顺序(如按资源 ID 排序),防止线程形成资源等待环 |
如何排查死锁
在 Java 中,死锁的排查可以采用以下几种方法:
1. 使用 jstack 工具
- 通过
jps -l
获取 Java 进程 PID。 - 执行
jstack <PID>
导出线程堆栈信息。 - 搜索
Found one Java-level deadlock:
判断是否发生死锁。
2. 使用 ThreadMXBean API
可以在程序中引入检测逻辑:
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadMXBean;
import java.lang.management.ThreadInfo;public class DeadlockChecker {public static void main(String[] args) {ThreadMXBean threadBean = ManagementFactory.getThreadMXBean();long[] deadlockedThreads = threadBean.findDeadlockedThreads();if (deadlockedThreads != null) {ThreadInfo[] infos = threadBean.getThreadInfo(deadlockedThreads);for (ThreadInfo info : infos) {System.out.println("发现死锁线程: " + info.getThreadName());}} else {System.out.println("未发现死锁");}}
}
3. 使用 IDE 工具
如 IntelliJ IDEA 调试时,可以在 Threads 面板查看线程状态,BLOCKED
的线程相互依赖时可能是死锁。
小结
死锁是多线程编程中的常见问题,理解其产生的四个条件及排查方法是关键。在实际开发中,建议使用超时锁、可中断锁、统一资源申请顺序等技术来避免死锁,从源头上降低风险。
必要条件/充分条件
必要条件(necessary condition)
如果 没有 A,那就一定没有 B。
换句话说:A 不成立 → B 不成立
但:A 成立 ≠ B 一定成立
类比:
空气是人活着的必要条件
没空气 → 人活不了
有空气 ≠ 人一定活着(还需要水、食物等)
充分条件(sufficient condition)
如果 有 A,那就一定有 B。
换句话说:A 成立 → B 一定成立
但:B 成立 ≠ A 一定成立
类比:
“下大雨”是“地面湿” 的充分条件
下大雨 → 地面必湿
地面湿 ≠ 一定是下雨(可能是洒水车路过)