一句话总结
死锁是指两个或多个线程互相持有对方需要的锁,导致所有线程永久阻塞。产生死锁必须同时满足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) 死锁检测:定时检查锁依赖图,发现环就中断一个线程。