在多线程编程中,死锁是最让人头疼的问题之一 —— 它让多个线程互相等待对方释放资源,最终所有线程都陷入永久阻塞,程序卡死且无法自行恢复。小到单机应用的功能异常,大到分布式系统的服务不可用,死锁都可能引发严重问题。
本文会从死锁的核心发生原因讲起,结合 Java 代码模拟死锁场景,用直观的表格和图文梳理死锁形成过程,再详细讲解死锁的预防策略和死锁发生后的解决办法,让你从根源理解并解决死锁问题。
一、什么是死锁?
先给死锁一个通俗定义:多个线程在执行过程中,因争夺共享资源而造成的一种互相等待的僵局,当死锁发生时,没有任何一个线程能主动打破这种局面,必须通过外部干预才能恢复。
举个生活中的例子:两个人过独木桥,一人从东上桥,一人从西上桥,走到桥中间时互相挡住对方。两人都要求对方先退回去,却都不肯自己后退,最终谁也走不过桥 —— 这就是典型的死锁,两人是 “线程”,独木桥是 “共享资源”。
二、死锁的发生原因
死锁的发生必须同时满足四个必要条件,缺一不可。这是理解死锁的核心,所有预防和解决死锁的方案,本质都是破坏这四个条件中的一个或多个。
2.1 四个必要条件(图文解析)
用一张图直观展示四个条件的关联,再逐一解释:

2.2 Java 代码模拟死锁 + 表格梳理执行过程
接下来用 Java 代码实现一个经典的死锁场景:两个线程,各自持有一个锁,又互相请求对方的锁,满足死锁的四个必要条件。
2.2.1 死锁模拟代码
/**
* 死锁模拟:两个线程,两把锁,互相等待对方释放锁
*/
public class DeadLockDemo {
// 定义两把共享锁(资源)
private static final Object LOCK1 = new Object();
private static final Object LOCK2 = new Object();
public static void main(String[] args) {
// 线程1:先获取LOCK1,再尝试获取LOCK2
Thread thread1 = new Thread(() -> {
synchronized (LOCK1) {
System.out.println(Thread.currentThread().getName() + " 已获取LOCK1,等待获取LOCK2");
try {
// 休眠100ms,确保线程2先获取LOCK2,让死锁必现
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
// 尝试获取LOCK2,此时LOCK2已被线程2持有
synchronized (LOCK2) {
System.out.println(Thread.currentThread().getName() + " 已获取LOCK2,执行完成");
}
}
}, "线程A");
// 线程2:先获取LOCK2,再尝试获取LOCK1
Thread thread2 = new Thread(() -> {
synchronized (LOCK2) {
System.out.println(Thread.currentThread().getName() + " 已获取LOCK2,等待获取LOCK1");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
// 尝试获取LOCK1,此时LOCK1已被线程1持有
synchronized (LOCK1) {
System.out.println(Thread.currentThread().getName() + " 已获取LOCK1,执行完成");
}
}
}, "线程B");
// 启动两个线程
thread1.start();
thread2.start();
}
}
2.2.2 执行结果
程序运行后会卡死,控制台输出如下,永远不会打印 “执行完成”:
线程A 已获取LOCK1,等待获取LOCK2
线程B 已获取LOCK2,等待获取LOCK1
2.2.3 表格梳理死锁形成过程(横表头 = 线程,竖表头 = 时刻)
用表格清晰展示每个时刻两个线程的资源持有和请求状态,让死锁形成过程一目了然:
| T1 | 启动,尝试获取 LOCK1 | 启动,尝试获取 LOCK2 | LOCK1、LOCK2 均未被占用 |
| T2 | 成功获取 LOCK1,执行 sleep (100) | 成功获取 LOCK2,执行 sleep (100) | LOCK1 被 A 持有,LOCK2 被 B 持有 |
| T3 | 休眠结束,尝试获取 LOCK2 | 休眠结束,尝试获取 LOCK1 | LOCK1 被 A 持有,LOCK2 被 B 持有 |
| T4 | 因 LOCK2 被 B 持有,进入阻塞状态 | 因 LOCK1 被 A 持有,进入阻塞状态 | 资源持有状态不变 |
| T5+ | 持续阻塞,等待 LOCK2 | 持续阻塞,等待 LOCK1 | 永久死锁,无任何线程释放资源 |
三、死锁的预防
死锁的预防是最理想的解决方案—— 在编码阶段就通过设计规避死锁,让死锁的四个必要条件无法同时满足。
核心思路:破坏死锁的四个必要条件中的一个或多个(互斥条件通常无法破坏,因为多数共享资源本身就需要互斥访问,比如数据库行锁、文件写锁),以下是可落地的预防策略,结合 Java 场景讲解。
3.1 破坏 “请求与保持” 条件
核心方案:线程在执行前,一次性获取所有需要的资源,如果有任何一个资源获取失败,就放弃已获取的资源(若有),直到所有资源都能获取到再执行。
Java 实现思路
比如需要 LOCK1 和 LOCK2 的线程,必须先同时尝试获取两把锁,而不是先拿一把再拿另一把。可以用CountDownLatch或自定义逻辑实现 “一次性获取资源”:
// 破坏请求与保持:一次性获取所有需要的锁
public class PreventDeadLock1 {
private static final Object LOCK1 = new Object();
private static final Object LOCK2 = new Object();
// 封装“一次性获取两把锁”的方法
private static void acquireAllLocks() {
synchronized (LOCK1) {
synchronized (LOCK2) {
// 执行需要两把锁的业务逻辑
System.out.println(Thread.currentThread().getName() + " 一次性获取LOCK1和LOCK2,执行业务");
}
}
}
public static void main(String[] args) {
new Thread(PreventDeadLock1::acquireAllLocks, "线程A").start();
new Thread(PreventDeadLock1::acquireAllLocks, "线程B").start();
}
}
效果:线程 A 先获取 LOCK1 和 LOCK2,线程 B 只能等待;线程 A 执行完释放后,线程 B 再获取,不会出现 “持一把等一把” 的情况。
3.2 破坏 “不剥夺条件”
核心方案:让线程在请求新资源失败时,主动释放已持有的资源,等待一段时间后重新尝试获取所有资源。
Java 实现思路
利用ReentrantLock(可重入锁)的可中断锁特性(lockInterruptibly()),当线程获取锁失败时,触发中断并释放已持有的锁,而不是一直阻塞:
import java.util.concurrent.locks.ReentrantLock;
// 破坏不剥夺条件:获取锁失败时释放已持有的锁
public class PreventDeadLock2 {
private static final ReentrantLock LOCK1 = new ReentrantLock();
private static final ReentrantLock LOCK2 = new ReentrantLock();
public static void main(String[] args) {
new Thread(() -> {
boolean lock1Acquired = false;
boolean lock2Acquired = false;
try {
// 尝试获取LOCK1
lock1Acquired = LOCK1.tryLock();
if (lock1Acquired) {
System.out.println(Thread.currentThread().getName() + " 获取LOCK1成功");
// 尝试获取LOCK2,失败则释放LOCK1
lock2Acquired = LOCK2.tryLock();
if (!lock2Acquired) {
System.out.println(Thread.currentThread().getName() + " 获取LOCK2失败,释放LOCK1");
LOCK1.unlock();
lock1Acquired = false;
// 等待100ms后重新尝试
Thread.sleep(100);
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
// 释放已持有的锁
if (lock1Acquired) LOCK1.unlock();
if (lock2Acquired) LOCK2.unlock();
}
if (lock1Acquired && lock2Acquired) {
System.out.println(Thread.currentThread().getName() + " 执行业务完成");
} else {
System.out.println(Thread.currentThread().getName() + " 重新尝试获取锁");
}
}, "线程A").start();
new Thread(() -> {
// 线程B逻辑与A一致
boolean lock1Acquired = false;
boolean lock2Acquired = false;
try {
lock2Acquired = LOCK2.tryLock();
if (lock2Acquired) {
System.out.println(Thread.currentThread().getName() + " 获取LOCK2成功");
lock1Acquired = LOCK1.tryLock();
if (!lock1Acquired) {
System.out.println(Thread.currentThread().getName() + " 获取LOCK1失败,释放LOCK2");
LOCK2.unlock();
lock2Acquired = false;
Thread.sleep(100);
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
if (lock1Acquired) LOCK1.unlock();
if (lock2Acquired) LOCK2.unlock();
}
}, "线程B").start();
}
}
效果:线程获取资源失败时,不会持有已有资源等待,而是主动释放,破坏 “不剥夺条件”(这里是主动放弃,而非强制剥夺,是 Java 中更易实现的方式)。
3.3 破坏 “循环等待” 条件(最常用、最易落地)
核心方案:为所有共享资源规定统一的获取顺序,所有线程都必须按照这个顺序获取资源,这样就不会形成资源请求的闭环。
这是实际开发中使用最多的策略,因为它实现简单,对程序性能影响最小。
Java 实现思路
比如给 LOCK1 和 LOCK2 编号(LOCK1 编号 1,LOCK2 编号 2),所有线程都先获取编号小的锁,再获取编号大的锁,改造之前的死锁代码:
// 破坏循环等待:按统一顺序获取锁(最常用)
public class PreventDeadLock3 {
private static final Object LOCK1 = new Object(); // 编号1
private static final Object LOCK2 = new Object(); // 编号2
public static void main(String[] args) {
// 线程1:按 LOCK1 -> LOCK2 顺序获取
Thread thread1 = new Thread(() -> {
synchronized (LOCK1) {
System.out.println(Thread.currentThread().getName() + " 已获取LOCK1,等待获取LOCK2");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
synchronized (LOCK2) {
System.out.println(Thread.currentThread().getName() + " 已获取LOCK2,执行完成");
}
}
}, "线程A");
// 线程2:同样按 LOCK1 -> LOCK2 顺序获取(不再先拿LOCK2)
Thread thread2 = new Thread(() -> {
synchronized (LOCK1) { // 统一先拿编号小的LOCK1
System.out.println(Thread.currentThread().getName() + " 已获取LOCK1,等待获取LOCK2");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
synchronized (LOCK2) {
System.out.println(Thread.currentThread().getName() + " 已获取LOCK2,执行完成");
}
}
}, "线程B");
thread1.start();
thread2.start();
}
}
执行结果:不会死锁,线程按顺序执行:
线程A 已获取LOCK1,等待获取LOCK2
线程A 已获取LOCK2,执行完成
线程B 已获取LOCK1,等待获取LOCK2
线程B 已获取LOCK2,执行完成
核心逻辑:所有线程都按统一顺序获取资源,永远不会形成 “你等我、我等你” 的循环,从根源上避免了循环等待条件。
四、死锁的解决(死锁发生后)
尽管我们可以通过预防策略减少死锁,但在复杂的多线程 / 分布式系统中,死锁仍有可能发生(比如资源数量动态变化、获取顺序难以统一)。此时需要有效的死锁解决办法,核心思路是检测到死锁后,通过外部干预打破死锁。
解决死锁分为两个步骤:死锁检测 → 死锁解除,以下结合 Java 应用和实际生产场景讲解。
4.1 第一步:死锁检测
需要先发现死锁,才能进行后续的解除操作。Java 提供了多种工具和 API 来检测死锁,从简单的命令行工具到代码级检测,满足不同场景需求。
4.1.1 命令行工具检测(最常用:jps + jstack)
这是排查 Java 应用死锁的首选方式,无需修改代码,操作简单,适合生产环境。操作步骤:
# 输出示例:1234 DeadLockDemo (1234是PID,DeadLockDemo是我们的死锁程序)
=============================
"线程B":
waiting to lock monitor 0x000000001f67f688 (object 0x000000076b601060, a java.lang.Object),
which is held by "线程A"
"线程A":
waiting to lock monitor 0x000000001f681908 (object 0x000000076b601070, a java.lang.Object),
which is held by "线程B"
Found 1 deadlock.
4.1.2 代码级检测(ThreadMXBean)
通过 Java 的ThreadMXBeanAPI,可以在程序中实时检测死锁,适合需要动态监控的场景:
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadMXBean;
// 代码级检测死锁
public class DeadLockDetector {
public static void main(String[] args) {
// 启动死锁程序
new Thread(DeadLockDemo::main).start();
// 定时检测死锁
new Thread(() -> {
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
while (true) {
// 获取死锁的线程ID数组
long[] deadlockThreadIds = threadMXBean.findDeadlockedThreads();
if (deadlockThreadIds != null && deadlockThreadIds.length > 0) {
System.out.println("检测到死锁,死锁线程数:" + deadlockThreadIds.length);
// 可以进一步获取死锁线程的详细信息
for (long tid : deadlockThreadIds) {
System.out.println("死锁线程ID:" + tid);
}
break;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}).start();
}
}
4.2 第二步:死锁解除
检测到死锁后,需要主动打破死锁,核心是让死锁的线程释放资源,常用的解除策略有以下 3 种,按生产环境推荐度从高到低排序:
4.2.1 终止线程(最常用:终止优先级低的线程)
核心方案:检测到死锁后,终止死锁闭环中优先级较低、业务影响较小的线程,让其释放持有的资源,打破死锁。
- Java 实现:通过Thread.stop()(不推荐,已废弃,会强制终止线程,可能导致资源未释放)或优雅中断(结合ReentrantLock的可中断锁,让线程响应中断并释放锁)。
- 生产场景:在分布式系统中,可通过 kill 进程、关闭服务实例、熔断降级等方式终止死锁的线程 / 服务。
- 优点:实现简单,见效快;缺点:可能丢失线程未执行完的业务数据,需保证业务幂等。
4.2.2 资源抢占(强制剥夺资源)
核心方案:从死锁的线程中强制剥夺部分资源,分配给其他线程,打破循环等待。
- Java 实现:利用ReentrantLock的tryLock(long timeout, TimeUnit unit)设置超时时间,线程获取锁超时后主动释放已持有的资源,相当于 “被剥夺”。
- 生产场景:比如数据库中,对长时间持有锁的事务进行超时回滚,释放数据库锁。
- 优点:无需终止线程,对业务影响较小;缺点:需要资源支持超时机制,实现稍复杂。
4.2.3 重启进程 / 服务(终极方案)
核心方案:如果死锁检测和解除失败,或死锁涉及多个进程 / 服务,直接重启发生死锁的进程或服务,强制释放所有资源。
- 适用场景:生产环境中死锁无法通过上述方式解除,且服务有高可用架构(比如集群),重启单个实例不影响整体服务。
- 优点:解决最彻底,无需复杂的解除逻辑;缺点:会导致服务短暂不可用,需做好容灾。
4.3 死锁解决的实战建议
五、死锁的相关拓展:避免与检测的区别
很多人会混淆死锁预防和死锁避免,这里做一个简单区分,让概念更清晰:
- 死锁预防:事前设计,破坏死锁的必要条件,让死锁根本无法发生;适合编码阶段,主动规避。
- 死锁避免:事中判断,在线程请求资源时,通过算法(比如银行家算法)判断是否会导致死锁,若会则拒绝资源请求;适合资源数量固定的场景(如操作系统内存分配),但 Java 中较少使用,因为实现复杂。
- 死锁解决:事后处理,检测到死锁后通过外部干预解除;适合死锁已经发生的场景。
六、总结
死锁是多线程编程的核心问题,但其解决思路非常清晰 ——围绕死锁的四个必要条件展开,从 “事前预防” 到 “事后解决” 形成完整的应对体系:
死锁并不可怕,只要理解其发生的本质,通过合理的设计和有效的排查手段,就能让我们的多线程程序远离死锁,保持高效稳定的运行。





