跳到主要内容

死锁排查

问题

线上出现线程死锁或数据库行锁死锁,如何排查和解决?

答案

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: 死锁的四个必要条件?

答案

  1. 互斥:资源同时只能一个线程持有
  2. 持有并等待:持有一个锁的同时等待另一个
  3. 不可剥夺:已获取的锁不能被其他线程强制释放
  4. 循环等待: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),检测到死锁后自动回滚代价最小的事务,另一个事务继续执行。应用层需捕获死锁异常并重试。

相关链接