死锁排查
问题
线上出现线程死锁或数据库行锁死锁,如何排查和解决?
答案
Java 线程死锁
jstack 诊断
jstack 自动检测死锁
jstack <pid>
# 输出末尾会有 Found one Java-level deadlock 信息
死锁堆栈示例
Found one Java-level deadlock:
=============================
"Thread-1":
waiting to lock monitor 0x00007f... (object 0x0000000..., a java.lang.Object),
which is held by "Thread-0"
"Thread-0":
waiting to lock monitor 0x00007f... (object 0x0000000..., a java.lang.Object),
which is held by "Thread-1"
死锁代码示例
典型死锁
Object lockA = new Object();
Object lockB = new Object();
// 线程 1: 先锁 A,再锁 B
new Thread(() -> {
synchronized (lockA) {
Thread.sleep(100);
synchronized (lockB) { /* ... */ } // 等 Thread-2 释放 B
}
}).start();
// 线程 2: 先锁 B,再锁 A
new Thread(() -> {
synchronized (lockB) {
Thread.sleep(100);
synchronized (lockA) { /* ... */ } // 等 Thread-1 释放 A
}
}).start();
预防策略
| 策略 | 说明 |
|---|---|
| 固定加锁顺序 | 所有线程按相同顺序获取锁 |
| 超时机制 | tryLock(timeout) 获取不到则放弃 |
| 减小锁粒度 | 避免一个方法内持有多把锁 |
| 使用并发工具 | 用 ConcurrentHashMap 代替 synchronized Map |
✅ 使用 tryLock 防止死锁
ReentrantLock lockA = new ReentrantLock();
ReentrantLock lockB = new ReentrantLock();
public void safeMethod() {
boolean gotA = false, gotB = false;
try {
gotA = lockA.tryLock(1, TimeUnit.SECONDS);
gotB = lockB.tryLock(1, TimeUnit.SECONDS);
if (gotA && gotB) {
// 业务逻辑
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
if (gotB) lockB.unlock();
if (gotA) lockA.unlock();
}
}
MySQL 行锁死锁
查看死锁日志
查看最近一次死锁
SHOW ENGINE INNODB STATUS;
-- 搜索 LATEST DETECTED DEADLOCK 部分
InnoDB 死锁日志
*** (1) TRANSACTION:
TRANSACTION 12345, ACTIVE 2 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 2 lock struct(s), heap size 1136
MySQL thread id 100, query id 200 updating
UPDATE orders SET status=2 WHERE id=1001
*** (2) TRANSACTION:
TRANSACTION 12346, ACTIVE 1 sec starting index read
UPDATE orders SET status=3 WHERE id=1002
*** WE ROLL BACK TRANSACTION (2)
常见 MySQL 死锁场景
场景 1:交叉更新
两个事务交叉更新不同行
-- 事务 1 -- 事务 2
UPDATE orders SET ... WHERE id=1; UPDATE orders SET ... WHERE id=2;
UPDATE orders SET ... WHERE id=2; UPDATE orders SET ... WHERE id=1;
-- 等事务 2 释放 id=2 的锁 -- 等事务 1 释放 id=1 的锁
-- → 死锁
解决:按主键顺序更新(先更新 id 小的)。
场景 2:间隙锁死锁
间隙锁冲突
-- 事务 1(RR 隔离级别)
SELECT * FROM orders WHERE amount > 100 FOR UPDATE; -- 加间隙锁
INSERT INTO orders (amount) VALUES (150); -- 等事务 2 的间隙锁
-- 事务 2
SELECT * FROM orders WHERE amount > 200 FOR UPDATE; -- 加间隙锁
INSERT INTO orders (amount) VALUES (250); -- 等事务 1 的间隙锁
解决:降低隔离级别为 RC(Read Committed),或缩小锁范围。
MySQL 死锁预防
| 策略 | 说明 |
|---|---|
| 按固定顺序访问表和行 | 相同业务按主键升序更新 |
| 缩短事务时间 | 避免大事务 |
| 降低隔离级别 | RC 比 RR 少间隙锁 |
| 合理设计索引 | 走索引减少锁范围 |
| 设置锁等待超时 | innodb_lock_wait_timeout=5 |
Arthas 死锁检测
Arthas 一键检测
# 找出阻塞其他线程的线程
thread -b
# 查看所有线程状态
thread --state BLOCKED
常见面试问题
Q1: 死锁的四个必要条件?
答案:
- 互斥:资源同时只能一个线程持有
- 持有并等待:持有一个锁的同时等待另一个
- 不可剥夺:已获取的锁不能被其他线程强制释放
- 循环等待:A 等 B,B 等 A
打破任意一个条件即可避免死锁。详见 死锁。
Q2: Java 如何检测死锁?
答案:
jstack自动检测ThreadMXBean.findDeadlockedThreads()- Arthas
thread -b
代码检测死锁
ThreadMXBean bean = ManagementFactory.getThreadMXBean();
long[] deadlockedThreads = bean.findDeadlockedThreads();
if (deadlockedThreads != null) {
ThreadInfo[] infos = bean.getThreadInfo(deadlockedThreads, true, true);
for (ThreadInfo info : infos) {
System.out.println(info);
}
}
Q3: MySQL 死锁后如何自动恢复?
答案:
InnoDB 有死锁检测机制(innodb_deadlock_detect=ON),检测到死锁后自动回滚代价最小的事务,另一个事务继续执行。应用层需捕获死锁异常并重试。