死锁
问题
什么是死锁?产生死锁的条件是什么?如何避免?
答案
死锁的定义
两个或多个线程/进程互相持有对方需要的资源,导致所有线程都无法继续执行。
死锁四个必要条件
同时满足以下四个条件才会发生死锁:
| 条件 | 说明 | 破坏方式 |
|---|---|---|
| 互斥 | 资源一次只能被一个线程持有 | 用无锁数据结构(CAS) |
| 持有并等待 | 持有资源的同时等待其他资源 | 一次性申请所有资源 |
| 不可抢占 | 资源不能被强制释放 | 超时释放 |
| 循环等待 | 形成等待环 | 按固定顺序加锁 |
Java 死锁示例
典型死锁代码
Object lock1 = new Object();
Object lock2 = new Object();
// 线程 A:先锁 1,再锁 2
new Thread(() -> {
synchronized (lock1) {
Thread.sleep(100); // 给线程 B 时间拿到 lock2
synchronized (lock2) { // 等待线程 B 释放 lock2 → 死锁
System.out.println("线程 A");
}
}
}).start();
// 线程 B:先锁 2,再锁 1
new Thread(() -> {
synchronized (lock2) {
Thread.sleep(100); // 给线程 A 时间拿到 lock1
synchronized (lock1) { // 等待线程 A 释放 lock1 → 死锁
System.out.println("线程 B");
}
}
}).start();
避免死锁
方案 1:按固定顺序加锁(推荐)
// 不管哪个线程,总是先锁 lock1 再锁 lock2
// 打破循环等待条件
synchronized (lock1) {
synchronized (lock2) {
// 业务逻辑
}
}
方案 2:使用 tryLock 超时
ReentrantLock lockA = new ReentrantLock();
ReentrantLock lockB = new ReentrantLock();
if (lockA.tryLock(1, TimeUnit.SECONDS)) {
try {
if (lockB.tryLock(1, TimeUnit.SECONDS)) {
try {
// 业务逻辑
} finally { lockB.unlock(); }
}
} finally { lockA.unlock(); }
}
排查死锁
# 方式 1:jstack 打印线程堆栈
jstack <pid> | grep -A 20 "deadlock"
# 方式 2:jconsole/jvisualvm 图形化工具
# 方式 3:代码中检测
ThreadMXBean bean = ManagementFactory.getThreadMXBean();
long[] threadIds = bean.findDeadlockedThreads();
常见面试问题
Q1: 如何排查线上死锁?
答案:
jstack <pid>:输出所有线程的堆栈,JVM 会自动检测死锁并打印- Arthas
thread -b:直接找到阻塞线程 - 看日志和监控:关注线程池是否打满、请求超时
Q2: 数据库死锁怎么处理?
答案:
MySQL InnoDB 有死锁检测机制(innodb_deadlock_detect),检测到死锁后自动回滚持有锁最少的事务。
预防方式:
- 按固定顺序访问表和行
- 大事务拆小事务
- 合理使用索引,减少锁范围
详见 MySQL 锁机制。