死锁的条件和如何排查?

2025年 阅读约 10 分钟 面试指南 · Java面试 · Java并发

深入解析死锁:4个必要条件(互斥、持有并等待、不可剥夺、循环等待)、手写死锁代码、jstack/jconsole/VisualVM排查方法、避免死锁的4种策略(顺序加锁、超时获取、死锁检测、减少锁粒度),附完整示例和面试模拟。

一句话总结

死锁是指两个或多个线程互相持有对方需要的锁,导致所有线程永久阻塞。产生死锁必须同时满足4 个条件:互斥、持有并等待、不可剥夺、循环等待。排查工具:jstack(查看线程堆栈,自动检测死锁)、jconsole、VisualVM。避免策略:顺序加锁(所有线程按相同顺序获取锁)、超时获取(tryLock)、死锁检测(定时检查锁依赖图)。

初级理解

什么是死锁?

死锁就像两个人吃饭,每人只有一根筷子,互相等对方放下筷子,结果谁也吃不了。

手写死锁代码

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) { System.out.println("线程1 获取 lockA"); try { Thread.sleep(100); } catch (Exception e) {} synchronized (lockB) { System.out.println("线程1 获取 lockB"); } } }, "线程1").start(); new Thread(() -> { synchronized (lockB) { System.out.println("线程2 获取 lockB"); try { Thread.sleep(100); } catch (Exception e) {} synchronized (lockA) { System.out.println("线程2 获取 lockA"); } } }, "线程2").start(); } } // 输出: // 线程1 获取 lockA // 线程2 获取 lockB // (然后两个线程永久阻塞)

死锁的 4 个必要条件

条件说明如何破坏
互斥资源同一时间只能被一个线程持有无法破坏(锁的本质)
持有并等待线程持有锁的同时等待其他锁一次性申请所有锁
不可剥夺线程持有的锁不能被强制释放用 tryLock 超时释放
循环等待线程间形成锁的环形依赖按固定顺序加锁
一句话总结:死锁 = 互斥 + 持有并等待 + 不可剥夺 + 循环等待,破坏任一条件即可避免。

中级深入

jstack 排查死锁

jstack 是 JDK 自带的线程堆栈分析工具,可以自动检测死锁

# 1. 找到 Java 进程 PID jps -l # 2. 打印线程堆栈(jstack 会自动检测死锁) jstack <pid> # jstack 输出末尾会显示: # Found one Java-level deadlock: # ============================= # "线程2": # waiting to lock monitor 0x00007f8a1c004e28 (object 0x000000076b8a9d78, a java.lang.Object), # which is held by "线程1" # "线程1": # waiting to lock monitor 0x00007f8a1c007898 (object 0x000000076b8a9d88, a java.lang.Object), # which is held by "线程2"

jconsole 可视化排查

jconsole 是 JDK 自带的图形化监控工具。在"线程"标签页点击"检测死锁",会自动列出所有死锁线程。

避免死锁 — 顺序加锁

// 方案1:按固定顺序加锁(最常用) public void method1() { synchronized (lockA) { synchronized (lockB) { // 业务逻辑 } } } public void method2() { synchronized (lockA) { // 先 lockA 再 lockB,与 method1 顺序一致 synchronized (lockB) { // 业务逻辑 } } } // 方案2:用 System.identityHashCode 确定顺序 if (System.identityHashCode(lockA) < System.identityHashCode(lockB)) { synchronized (lockA) { synchronized (lockB) { /* ... */ } } } else { synchronized (lockB) { synchronized (lockA) { /* ... */ } } }

避免死锁 — 超时获取

// 用 ReentrantLock.tryLock() 超时获取 public boolean transfer(Account from, Account to, int amount) { while (true) { if (from.lock.tryLock()) { try { if (to.lock.tryLock(1, TimeUnit.SECONDS)) { try { from.debit(amount); to.credit(amount); return true; } finally { to.lock.unlock(); } } } finally { from.lock.unlock(); } } // 没获取到全部锁,随机等待后重试 Thread.sleep(random.nextInt(100)); } }
中级要点:jstack 自动检测死锁,顺序加锁是最简单有效的避免方式,tryLock 超时是兜底方案。

高级拓展

活锁(Livelock)

活锁是指线程虽然没有阻塞,但一直在重复尝试-失败-重试,无法推进。例如两个线程互相谦让资源,导致谁也拿不到。

// 活锁示例:两个线程互相谦让 // 线程1:发现线程2需要资源 → 释放 → 等待 // 线程2:发现线程1需要资源 → 释放 → 等待 // 两个线程都在"谦让",谁也拿不到资源

饥饿(Starvation)

饥饿是指线程一直得不到执行。例如非公平锁下,某些线程可能永远获取不到锁。解决方案:使用公平锁。

死锁检测算法

可以通过资源分配图检测死锁:如果锁的等待关系形成环,则存在死锁。ThreadMXBean 可以编程方式检测:

// 编程方式检测死锁 ThreadMXBean mbean = ManagementFactory.getThreadMXBean(); long[] deadlockedThreads = mbean.findDeadlockedThreads(); if (deadlockedThreads != null) { for (long id : deadlockedThreads) { ThreadInfo info = mbean.getThreadInfo(id); System.out.println("死锁线程: " + info.getThreadName()); } }

数据库死锁

数据库也会发生死锁(如 MySQL InnoDB)。数据库通常有死锁检测机制,检测到死锁后会回滚其中一个事务来解除死锁。

高级加分项:能区分死锁、活锁、饥饿,知道 ThreadMXBean 编程检测死锁,了解数据库死锁的处理方式。

实战场景

场景一:转账死锁

// 问题:A→B 和 B→A 同时转账可能死锁 // 解决:按账户 ID 排序加锁 public void transfer(Account from, Account to, int amount) { Account first = from.id < to.id ? from : to; Account second = from.id < to.id ? to : from; synchronized (first) { synchronized (second) { from.debit(amount); to.credit(amount); } } }

场景二:线程池死锁

// 问题:线程池中的任务互相等待 ExecutorService pool = Executors.newFixedThreadPool(2); pool.submit(() -> { Future<?> f = pool.submit(() -> "result"); f.get(); // 等待另一个任务完成 }); // 两个任务互相等待 → 线程池死锁(所有线程都被占用)

常见坑

原因解决方案
嵌套锁顺序不一致不同方法以不同顺序获取锁统一加锁顺序
线程池中任务互相等待任务 A 等待任务 B,B 等不到线程避免任务间依赖,增大线程池
synchronized 中调用外部方法外部方法可能获取其他锁缩小同步范围,避免嵌套

面试模拟

Q:死锁的 4 个必要条件?

A:1) 互斥:资源只能被一个线程持有;2) 持有并等待:线程持有锁的同时等待其他锁;3) 不可剥夺:锁不能被强制释放;4) 循环等待:线程间形成锁的环形依赖。破坏任一条件即可避免死锁。

Q:如何排查死锁?

A:1) jstack <pid>:打印线程堆栈,末尾自动显示死锁信息;2) jconsole:图形化工具,线程标签页点击"检测死锁";3) ThreadMXBean:编程方式 findDeadlockedThreads();4) 线上环境可以先 jstack 导出堆栈再分析。

Q:如何避免死锁?

A:1) 顺序加锁:所有线程按相同顺序获取锁(最常用);2) 超时获取:tryLock(timeout) 超时放弃;3) 一次性申请:用一个大锁代替多个小锁(降低并发性);4) 死锁检测:定时检查锁依赖图,发现环就中断一个线程。