什么是线程死锁,如何排查?如何解决?
2026年01月12日
一则或许对你有用的小广告
欢迎 加入小哈的星球 ,你将获得: 专属的项目实战(已更新的所有项目都能学习) / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论
- 新开坑项目: 《Spring AI 项目实战(问答机器人、RAG 增强检索、联网搜索)》 正在持续爆肝中,基于
Spring AI + Spring Boot3.x + JDK 21..., 点击查看; - 《从零手撸:仿小红书(微服务架构)》 已完结,基于
Spring Cloud Alibaba + Spring Boot3.x + JDK 17..., 点击查看项目介绍; 演示链接: http://116.62.199.48:7070/; - 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/
面试考察点
面试官提出这个问题,通常想考察以下几个层面:
- 基础概念理解:你是否能清晰、准确地定义线程死锁,并理解其发生的本质原因。
- 理论掌握深度:你是否掌握导致死锁发生的 四个必要条件,这是分析和预防死锁的理论基石。
- 实战排查能力:当线上系统出现疑似死锁时,你是否知道如何利用工具(如
jstack)进行诊断和定位,这是高级工程师必备的故障排查技能。 - 解决方案与设计思维:面试官不仅仅想知道如何“解决”一个已发生的死锁,更想考察你如何在系统设计阶段就预防死锁的发生,这体现了你的并发编程功底和系统设计意识。
核心答案
线程死锁 是指两个或两个以上的线程在执行过程中,因争夺共享资源而造成的一种互相等待的现象。若无外力干涉,这些线程都将无法向前推进。
排查死锁 主要依赖 JVM 提供的命令行工具:
- 使用
jstack -l <pid>命令 dump 出线程栈信息。jstack会自动检测并报告发现的死锁,明确指出哪些线程、在等待哪个锁、当前持有哪个锁。 - 使用 JConsole 或 VisualVM 这类图形化工具,它们提供了“检测死锁”的按钮,可以直观地查看死锁线程的依赖关系图。
解决和预防死锁 的核心在于破坏其产生的必要条件:
- 避免嵌套锁:尽量只获取一个锁。如果必须使用多个锁,则强制规定所有线程以相同的全局顺序获取锁(例如,按锁对象的 HashCode 排序)。
- 使用带超时的锁:如
Lock.tryLock(long, TimeUnit),获取锁失败时进行回退、重试或记录告警,而不是无限等待。 - 死锁检测与恢复:对于已发生的死锁,在检测到后,可以中断(
Thread.interrupt())或终止其中一个线程,强制释放资源。
深度解析
原理/机制:四个必要条件
死锁的发生必须同时满足以下四个条件,缺一不可:
- 互斥条件:资源是独占的,一次只能被一个线程持有。
- 占有且等待:线程已经持有了至少一个资源,并在等待获取其他线程持有的资源时,不会释放自己已占有的资源。
- 不可剥夺条件:线程已获得的资源在未使用完之前,不能被其他线程强行抢占。
- 循环等待条件:存在一个线程-资源的环形等待链。例如,线程 A 持有锁 1 等待锁 2,线程 B 持有锁 2 等待锁 1。
“哲学家就餐问题” 是诠释这四个条件的经典模型。
代码示例
下面是一个必然会发生死锁的典型代码:
public class DeadLockDemo {
private static final Object lockA = new Object();
private static final Object lockB = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (lockA) { // 线程1获取lockA
System.out.println("Thread-1: Holding lock A...");
try { Thread.sleep(10); } catch (InterruptedException e) {}
System.out.println("Thread-1: Waiting for lock B...");
synchronized (lockB) { // 线程1尝试获取lockB,但此时它被线程2持有
System.out.println("Thread-1: Acquired both locks!");
}
}
}).start();
new Thread(() -> {
synchronized (lockB) { // 线程2获取lockB
System.out.println("Thread-2: Holding lock B...");
try { Thread.sleep(10); } catch (InterruptedException e) {}
System.out.println("Thread-2: Waiting for lock A...");
synchronized (lockA) { // 线程2尝试获取lockA,但此时它被线程1持有
System.out.println("Thread-2: Acquired both locks!");
}
}
}).start();
}
}
运行此程序,两个线程将卡住,使用 jstack 查看会看到清晰的死锁报告。
最佳实践与解决方案
- 固定锁顺序(破坏循环等待):这是最常用且有效的预防策略。为所有需要竞争的锁定义一个全局的获取顺序(例如,按
System.identityHashCode排序,但需处理 Hash 冲突),所有线程都必须遵守这个顺序。// 示例:按对象HashCode决定获取顺序 void transfer(Account from, Account to, int amount) { Object firstLock = from.hashCode() < to.hashCode() ? from : to; Object secondLock = firstLock == from ? to : from; synchronized (firstLock) { synchronized (secondLock) { // ... 转账操作 } } } - 使用尝试锁(破坏占有且等待):通过
ReentrantLock的tryLock方法。if (lockA.tryLock(100, TimeUnit.MILLISECONDS)) { try { if (lockB.tryLock(100, TimeUnit.MILLISECONDS)) { try { // 成功获取两把锁,执行业务 } finally { lockB.unlock(); } } } finally { lockA.unlock(); } } else { // 获取锁失败,进行回退、重试或记录日志 log.warn("Failed to acquire locks, rollback or retry."); } - 开放调用:在调用外部方法(尤其是可能获取其他锁的方法)前,释放自己持有的锁。这需要仔细设计,可能降低并发性。
常见误区
- 误区一:“
synchronized不会产生死锁”。错,synchronized正是因其 “占有且等待” 和 “不可剥夺” 的特性,很容易导致死锁。 - 误区二:“只要不用嵌套锁就安全”。错,在复杂的调用链路中(如 A->B->C),可能间接形成锁的嵌套,同样危险。
- 误区三:“用
jstack看到线程 BLOCKED 就是死锁”。不一定,BLOCKED 状态只说明线程在等待进入同步块,可能是正常的锁竞争。死锁的关键特征是jstack输出中明确指出的 “Found 1 deadlock” 及后续的循环等待详情。
总结
线程死锁是并发编程中的经典难题,理解其 四个必要条件 是预防的根本,熟练使用 jstack 等工具 是线上排查的利剑,而在设计时遵循 固定锁顺序、使用尝试锁 等最佳实践,则是从根本上规避风险的关键。