《Java 100 天进阶之路》第56篇:死锁与排查工具(2026版)
第56篇:死锁与排查工具(2026版)
📌系列导航:《Java 100 天进阶之路》完整目录 |
⬅️ 上一篇:第55篇:线程池ThreadPoolExecutor |
➡️ 下一篇:第57篇:CompletableFuture异步编程(待发布)
🗺️ 本文阅读地图(3 分钟速览)
第55篇拿下了线程池,但线程池用不好也会出问题——死锁就是最严重的线程阻塞事故。本篇彻底搞懂死锁的成因、排查和预防:
| 模块 | 核心问题 | 一句话回答 |
|---|---|---|
| 死锁是什么 | 为什么线程会“卡死”? | 两个或多个线程互相持有对方需要的资源,形成循环等待,全部永久阻塞 |
| 四大必要条件 | 死锁怎么产生的? | 互斥 + 持有并等待 + 不可剥夺 + 循环等待 |
| 怎么排查 | 线上死锁怎么定位? | jstack -l <pid>直接输出“Found one Java-level deadlock” |
| 怎么预防 | 如何避免死锁? | 破坏四大条件之一——统一锁顺序、tryLock超时、减少锁粒度 |
| 面试最爱问 | 高频考点有哪些? | 见文末 面试 小节 |
文章目录
- 第56篇:死锁与排查工具(2026版)
- 🗺️ 本文阅读地图(3 分钟速览)
- 一、核心知识点
- 二、生活类比:从“十字路口”到“死锁”
- 三、死锁代码示例
- 四、死锁排查工具
- 4.1 jstack —— 命令行排查(首选)
- 4.2 jconsole —— 图形化排查
- 4.3 VisualVM —— 深度分析
- 4.4 排查工具对比
- 五、死锁预防策略
- 5.1 破坏“循环等待”——统一锁顺序
- 5.2 破坏“持有并等待”——一次性申请所有资源
- 5.3 破坏“不可剥夺”——使用可中断/超时锁
- 5.4 破坏“互斥”——使用无锁/读写锁
- 5.5 减少锁粒度
- 六、生产级避坑清单
- 七、面试高频考点
- 面试官追问陷阱(加分题)
- 八、练习题
- 📊 你的学习进度
- 👉 下一篇文章预告
一、核心知识点
死锁是什么?
死锁是指两个或多个线程在执行过程中,因争夺共享资源而造成的一种互相等待的状态,导致所有相关线程都无法继续执行。死锁是并发编程中最严重的故障之一——不会自动恢复,只能人工干预。
死锁 vs 活锁 vs 饥饿:
| 术语 | 定义 | 特点 |
|---|---|---|
| 死锁(Deadlock) | 线程互相持有对方需要的资源,永久阻塞 | 不主动干预永不恢复 |
| 活锁(Livelock) | 线程不断重试但总是失败,无法推进 | 线程不阻塞但“空转” |
| 饥饿(Starvation) | 低优先级线程长期抢不到资源 | 线程有进展但极其缓慢 |
死锁的四大必要条件(Coffman条件):
死锁的发生必须同时满足以下四个条件,缺一不可:
| 条件 | 含义 | Java 中的体现 |
|---|---|---|
| 互斥(Mutual Exclusion) | 资源同一时刻只能被一个线程占用 | synchronized / ReentrantLock 的排他性 |
| 持有并等待(Hold and Wait) | 线程持有资源的同时还在等待其他资源 | 嵌套 synchronized 块 |
| 不可剥夺(No Preemption) | 线程持有的资源只能主动释放,不能被强行剥夺 | Java 锁不支持外部强制解锁 |
| 循环等待(Circular Wait) | 线程之间形成等待环路 | A等B、B等A |
💡核心结论:只要破坏四个条件中的任意一个,死锁就不可能发生。
常见死锁场景:
- 嵌套锁(最经典):线程A持有锁1等待锁2,线程B持有锁2等待锁1
- 数据库连接池耗尽:所有连接被占用,新请求等待连接,已持有连接的线程又等待其他资源
- 生产者-消费者死锁:生产者等待消费者消费,消费者等待生产者生产
- 线程池中任务互相等待:子任务提交到同一线程池并等待结果
二、生活类比:从“十字路口”到“死锁”
死锁就像四个方向的车在十字路口互不相让:
- 每个方向都有一辆车(线程),占据了一条车道(资源)。
- 每辆车都在等待前面那辆车让路(等待资源),但前面那辆车也在等更前面的车。
- 四辆车形成了环形等待,谁也动不了——这就是死锁。
四大必要条件对应:
- 互斥:一条车道一次只能过一辆车。
- 持有并等待:每辆车占着当前车道,同时等着前面车道空出来。
- 不可剥夺:不能把前面的车强行拖走。
- 循环等待:A等B,B等C,C等D,D等A。
排查死锁就像交警处理堵车:
jstack:交警拿着手电筒逐一检查每辆车的状态和位置。jconsole/VisualVM:交通指挥中心的大屏幕,一图看清所有车的堵塞关系。
预防死锁就像交通规则设计:
- 统一锁顺序:所有车按“先直行、再左转”的统一规则行驶。
- 定时锁:每辆车设置“等待超时,先让其他车走”。
- 减少锁粒度:把大十字路口拆成多个小路口,减少相互影响。
三、死锁代码示例
以下代码模拟了最经典的“两线程、两资源互持互等”死锁场景,是面试最常考的死锁案例:
publicclassDeadlockDemo{privatestaticfinalObjectLOCK_A=newObject();privatestaticfinalObjectLOCK_B=newObject();publicstaticvoidmain(String[]args){Threadt1=newThread(()->{synchronized(LOCK_A){System.out.println("线程1: 持有锁A,等待锁B...");try{Thread.sleep(100);}catch(InterruptedExceptione){}synchronized(LOCK_B){System.out.println("线程1: 获取锁B!");}}});Threadt2=newThread(()->{synchronized(LOCK_B){System.out.println("线程2: 持有锁B,等待锁A...");try{Thread.sleep(100);}catch(InterruptedExceptione){}synchronized(LOCK_A){System.out.println("线程2: 获取锁A!");}}});t1.start();t2.start();}}运行结果:
线程1:持有锁A,等待锁B...线程2:持有锁B,等待锁A...(程序卡死,永不退出)四、死锁排查工具
4.1 jstack —— 命令行排查(首选)
jstack是 JDK 自带的命令行工具,用于生成 Java 进程的线程快照(Thread Dump)。
排查步骤:
# 1. 找到 Java 进程的 PIDjps-l# 输出:12345 com.example.DeadlockDemo# 2. 生成线程快照(-l 选项会额外打印锁信息)jstack-l12345jstack 输出中定位死锁的关键信息:
FoundoneJava-level deadlock:============================="Thread-1":waitingtolockmonitor0x000000076b8a3a00(object0x000000076c0019d8,ajava.lang.Object),which is held by"Thread-0""Thread-0":waitingtolockmonitor0x000000076b8a3b00(object0x000000076c0019e8,ajava.lang.Object),which is held by"Thread-1"Javastack informationforthe threads listed above:==================================================="Thread-1":atDeadlockDemo.lambda$main$1(DeadlockDemo.java:18)-waitingtolock<0x000000076c0019d8>(ajava.lang.Object)-locked<0x000000076c0019e8>(ajava.lang.Object)关键信息解读:
Found one Java-level deadlock:明确告知存在死锁waiting to lock:当前线程正在等待哪把锁which is held by:这把锁被哪个线程持有- 堆栈信息中的文件名和行号:精准定位死锁代码位置
💡生产环境最佳实践:
jstack -l <pid>中的-l选项会打印java.util.concurrent.locks的锁信息,排查ReentrantLock死锁时必须加上。
4.2 jconsole —— 图形化排查
jconsole是 JDK 自带的图形化监控工具。
操作步骤:
- 启动
jconsole,连接目标 Java 进程 - 切换到“线程”页签
- 点击“检测死锁”按钮
- 如果存在死锁,工具会自动高亮死锁线程,并展示锁依赖关系
优势:一键检测,可视化展示锁依赖关系图,无需记忆命令行参数。
4.3 VisualVM —— 深度分析
VisualVM 是功能更强大的图形化工具,集成了 jstack、jconsole 等工具的能力。
核心功能:
- 线程状态实时监控
- 死锁自动检测
- CPU/内存抽样分析
- 线程 Dump 导出与对比
适用场景:本地开发和测试环境,需要深入分析线程行为模式时。
4.4 排查工具对比
| 工具 | 类型 | 适用场景 | 特点 |
|---|---|---|---|
| jstack | 命令行 | 生产环境首选 | 轻量、快速、可直接输出死锁信息 |
| jconsole | 图形化 | 测试/预发布环境 | 一键检测死锁,可视化锁依赖 |
| VisualVM | 图形化 | 深度分析 | 功能最全面,可追踪线程行为模式 |
| Arthas | 命令行 | 生产环境高级排查 | 阿里开源,功能强大,可在线诊断 |
五、死锁预防策略
根据“破坏四大必要条件”的思路,有以下预防策略:
5.1 破坏“循环等待”——统一锁顺序
核心思想:所有线程按固定的全局顺序获取锁。
示例:规定必须先获取锁A,再获取锁B。
// ✅ 正确:所有线程按相同顺序加锁publicvoidsafeMethod(){synchronized(LOCK_A){// 先Asynchronized(LOCK_B){// 后B// 业务逻辑}}}如果业务上无法统一顺序,可采用资源编号法:为所有资源分配唯一编号,强制按编号升序获取。
5.2 破坏“持有并等待”——一次性申请所有资源
核心思想:线程在开始执行前,一次性申请所有需要的资源,全部成功才执行,否则释放已持有的资源并重试。
Java 实现:使用ReentrantLock的tryLock尝试获取所有锁,失败则释放已获取的锁。
LocklockA=newReentrantLock();LocklockB=newReentrantLock();publicbooleantryAcquireBoth(){if(lockA.tryLock()){try{if(lockB.tryLock()){returntrue;// 两把锁都拿到}}finally{if(!lockB.isHeldByCurrentThread()){lockA.unlock();// 获取B失败,释放A}}}returnfalse;}5.3 破坏“不可剥夺”——使用可中断/超时锁
核心思想:使用ReentrantLock的lockInterruptibly()或tryLock(timeout),让线程在等待锁时可以被中断或超时放弃。
Locklock=newReentrantLock();try{if(lock.tryLock(3,TimeUnit.SECONDS)){try{// 业务逻辑}finally{lock.unlock();}}else{// 超时处理——放弃或重试}}catch(InterruptedExceptione){Thread.currentThread().interrupt();}5.4 破坏“互斥”——使用无锁/读写锁
互斥条件很难完全破坏——synchronized和ReentrantLock本身就是互斥的。但在部分场景下可以通过以下方式缓解:
- 读写锁(
ReentrantReadWriteLock):读锁共享,减少锁竞争 - CAS 无锁(
Atomic*类):使用乐观锁替代悲观锁 - CopyOnWrite 容器:读多写少场景
5.5 减少锁粒度
- 使用
ConcurrentHashMap替代HashMap+synchronized - 使用
ConcurrentLinkedQueue替代手动加锁的队列 - 避免在锁内调用外部方法(可能触发未知阻塞)
六、生产级避坑清单
✅ 死锁预防与排查避坑指南 1. 避免嵌套 synchronized → 能用同步块就别锁整个方法 2. 统一锁顺序 → 多个锁必须按固定顺序获取 3. 使用 ReentrantLock.tryLock(timeout) → 获取失败时释放已持有的锁并重试或降级 4. 减少锁持有时间 → 只在必要时加锁,尽快释放 5. 避免在锁内调用外部方法 → 外部方法可能触发未知阻塞 6. 线程池任务中避免相互等待 → 子任务不要向同一线程池提交并等待结果 7. 线上排查三件套:jps → jstack -l → 定位代码行号 8. 生产环境保留线程 Dump → 事故复盘必备线上死锁快速排查流程:
1. jps -l → 找到问题进程 PID 2. jstack -l <PID> → 生成线程快照,搜索 "deadlock" 3. 定位代码行号 → 找到 "waiting to lock" 和 "locked" 对应的行号 4. 分析锁顺序 → 检查两个线程获取锁的顺序是否相反 5. 修复并验证 → 统一锁顺序或用 tryLock 替代七、面试高频考点
Q1:死锁的四个必要条件是什么?
死锁必须同时满足四个条件:①互斥:资源一次只能被一个线程占用;②持有并等待:线程持有资源的同时还在等待其他资源;③不可剥夺:线程持有的资源只能主动释放,不能被强行剥夺;④循环等待:线程之间形成等待环路。只要破坏任意一个,死锁就不会发生。
Q2:如何排查 Java 死锁?
① 使用
jps找到 Java 进程 PID;② 执行jstack -l <PID>生成线程快照;③ 查找输出中的Found one Java-level deadlock;④ 分析waiting to lock和locked信息,定位代码行号。也可用jconsole或 VisualVM 的“检测死锁”按钮一键检测。
Q3:如何预防死锁?
核心是破坏四个必要条件之一:①破坏循环等待:所有线程按固定顺序获取锁;②破坏持有并等待:一次性申请所有资源,失败则全部释放;③破坏不可剥夺:使用
ReentrantLock.tryLock(timeout)超时机制;④减少锁粒度:使用 ConcurrentHashMap 等并发容器。
Q4:synchronized 和 ReentrantLock 在死锁预防上有什么区别?
synchronized 无法中断、无法超时,发生死锁后只能通过
jstack发现并重启。ReentrantLock 支持tryLock(timeout)和lockInterruptibly(),可以通过超时中断打破死锁,在预防死锁上更灵活。
Q5:活锁和死锁有什么区别?
死锁是线程互相等待资源,永久阻塞,不主动干预永不恢复。活锁是线程不阻塞,但不断重试失败,无法推进任务,虽然“活着”但“空转”。活锁通常通过引入随机退避(如指数退避)来解决。
面试官追问陷阱(加分题)
追问1:“使用ReentrantLock.tryLock()一定能避免死锁吗?”
👉不一定。
tryLock()只能避免无限等待导致的死锁,但如果业务逻辑要求“两把锁必须同时持有才能操作”,使用tryLock后需要正确处理“部分获取成功”的情况——获取锁A成功但获取锁B失败时,必须释放锁A并重试,否则仍可能死锁。
追问2:“jstack输出中的Found one Java-level deadlock一定准确吗?”
👉不一定。
jstack只能检测到已经发生的死锁。有些死锁是间歇性的(如特定时序下触发),jstack抓取时可能恰好没有发生。而且jstack只能检测synchronized和ReentrantLock的死锁,无法检测更复杂的资源死锁(如数据库连接池耗尽)。
追问3:“数据库死锁和 Java 死锁有什么区别?”
👉 Java 死锁是 JVM 层面的线程锁竞争,由
jstack排查。数据库死锁是事务级别的锁竞争(行锁、间隙锁),由数据库自己检测并回滚其中一个事务。两者互不直接关联——Java 代码中的synchronized不会导致 MySQL 死锁,反之亦然。
八、练习题
代码题:编写一个会产生死锁的 Java 程序,并使用
jstack定位死锁线程和代码行号。💡 思路:两个线程以相反顺序获取两把锁,运行后用
jps找 PID,jstack -l <PID>查看死锁信息。分析题:某系统使用
synchronized保护共享资源,偶尔出现线程卡死。如何在不重启的情况下确认是否发生了死锁?💡 思路:使用
jstack -l <PID>生成线程快照,搜索deadlock关键字。如果确认死锁,分析锁顺序,考虑用ReentrantLock.tryLock(timeout)替代。场景设计:一个支付系统需要同时锁定账户A和账户B进行转账。如何设计加锁策略避免死锁?
💡 思路:按账户 ID 排序,总是先锁 ID 小的账户,再锁 ID 大的账户,破坏循环等待条件。或使用
ReentrantLock.tryLock尝试获取两把锁,失败则全部释放并重试。
📊 你的学习进度
- 当前:第56篇 / 共108篇 ·进阶篇:并发编程与JUC详解(第51~60篇)
- ✅ 已完成:基础篇44篇 + 第45~56篇
- 📖 正在学:第56篇
- ⏳ 待学习:第57~108篇
👉 📚 完整目录 & 学习指南 | 🔥 订阅本专栏,不错过每一篇
👉 下一篇文章预告
🚀下一篇:《第57篇:CompletableFuture异步编程》
内容简介:CompletableFuture 的核心 API(thenApply、thenCompose、allOf、anyOf)、异步任务编排、异常处理、超时控制与线程池配置。
👉多线程专题持续深入,拿下异步编程!
📌《Java 100 天进阶之路 | 从入门到上岗就业》每天一篇,建议收藏 + 关注,一起100天拿offer!
👉 点击关注我,更新后第一时间收到推送!
