一、什么是死锁?💀
经典比喻:两个人过独木桥
桥A 桥B
│ │
┌──┴──┐ ┌──┴──┐
│ 🚶♂️ │←─────────→│ 🚶♀️ │
└─────┘ └─────┘
双方都不让步,谁也过不去!这就是死锁!
代码演示
public class DeadLockDemo {
private static Object lockA = new Object();
private static Object lockB = new Object();
public static void main(String[] args) {
// 线程1:先获取lockA,再获取lockB
new Thread(() -> {
synchronized (lockA) {
System.out.println("线程1:获取了lockA");
try { Thread.sleep(100); } catch (Exception e) {}
System.out.println("线程1:等待lockB...");
synchronized (lockB) { // ← 永远等不到
System.out.println("线程1:获取了lockB");
}
}
}, "Thread-1").start();
// 线程2:先获取lockB,再获取lockA
new Thread(() -> {
synchronized (lockB) {
System.out.println("线程2:获取了lockB");
try { Thread.sleep(100); } catch (Exception e) {}
System.out.println("线程2:等待lockA...");
synchronized (lockA) { // ← 永远等不到
System.out.println("线程2:获取了lockA");
}
}
}, "Thread-2").start();
}
}
输出:
线程1:获取了lockA
线程2:获取了lockB
线程1:等待lockB...
线程2:等待lockA...
(程序卡死,永远等待)💀
死锁的四个必要条件 🔐
死锁必须同时满足以下四个条件:
1️⃣ 互斥条件(Mutual Exclusion)
资源同一时间只能被一个线程占用
🔒 = 锁只能一个人拿
2️⃣ 持有并等待(Hold and Wait)
线程持有资源的同时,还在等待其他资源
🔒A在手,等🔒B
3️⃣ 不可剥夺(No Preemption)
资源不能被强制抢走,只能主动释放
🔒不能抢,只能自己放
4️⃣ 循环等待(Circular Wait)
存在循环等待资源的关系链
线程1等线程2,线程2等线程1
T1 → 🔒A → T2 → 🔒B → T1 (循环!)
生活比喻:
四人吃饭,每人需要左右两根筷子🥢
1️⃣ 互斥:一根筷子同时只能被一个人拿
2️⃣ 持有并等待:拿了左边的筷子,等右边的筷子
3️⃣ 不可剥夺:筷子不能抢,只能等别人放下
4️⃣ 循环等待:
A拿了筷子1,等筷子2
B拿了筷子2,等筷子3
C拿了筷子3,等筷子4
D拿了筷子4,等筷子1
形成循环!大家都饿死了!😭
二、线上死锁发现:症状识别 🔍
症状1:系统突然卡住
正常:
请求响应时间:50ms
TPS:1000
异常:
请求响应时间:∞ (超时)
TPS:0
CPU:0%(线程都在等待,不消耗CPU)
症状2:线程池耗尽
监控告警:
[ERROR] ThreadPool is exhausted!
- Active Threads: 200/200
- Queue Size: 10000
- All threads in WAITING state
症状3:特定接口100%超时
日志:
2025-01-01 10:00:00 /api/transfer - TIMEOUT (30s)
2025-01-01 10:00:30 /api/transfer - TIMEOUT (30s)
2025-01-01 10:01:00 /api/transfer - TIMEOUT (30s)
...
三、快速定位:工具实战 🛠️
方法1:jstack分析线程堆栈(最常用!)
步骤1:找到Java进程PID
# 方法1:jps
jps -l
# 输出:
# 12345 com.example.Application
# 方法2:ps
ps -ef | grep java
# 方法3:top
top
# 然后按 Shift + H 查看线程
步骤2:生成线程dump
# 方法1:jstack
jstack -l 12345 > thread_dump.txt
# 方法2:kill -3(不会真的杀进程)
kill -3 12345
# 线程dump会输出到catalina.out(Tomcat)
# 方法3:jcmd
jcmd 12345 Thread.print > thread_dump.txt
步骤3:分析dump文件
正常线程:
"Thread-1" #12 prio=5 os_prio=0 tid=0x00007f8c2c001000 nid=0x1234 runnable
java.lang.Thread.State: RUNNABLE
at java.io.FileInputStream.readBytes(Native Method)
...
死锁线程:
"Thread-1" #12 prio=5 os_prio=0 tid=0x00007f8c2c001000 nid=0x1234 waiting for monitor entry
java.lang.Thread.State: BLOCKED (on object monitor)
at com.example.DeadLock.lambda$main$0(DeadLock.java:20)
- waiting to lock <0x00000000d5f5e9a0> (a java.lang.Object) ← lockB
- locked <0x00000000d5f5e990> (a java.lang.Object) ← lockA
...
"Thread-2" #13 prio=5 os_prio=0 tid=0x00007f8c2c002000 nid=0x1235 waiting for monitor entry
java.lang.Thread.State: BLOCKED (on object monitor)
at com.example.DeadLock.lambda$main$1(DeadLock.java:32)
- waiting to lock <0x00000000d5f5e990> (a java.lang.Object) ← lockA
- locked <0x00000000d5f5e9a0> (a java.lang.Object) ← lockB
...
关键信息:
线程1:
- locked <0xAAA> (lockA) ✅ 持有
- waiting <0xBBB> (lockB) ⏳ 等待
线程2:
- locked <0xBBB> (lockB) ✅ 持有
- waiting <0xAAA> (lockA) ⏳ 等待
→ 形成死锁!
步骤4:jstack自动检测死锁
jstack会在输出末尾自动检测死锁:
Found one Java-level deadlock:
=============================
"Thread-2":
waiting to lock monitor 0x00007f8c2c0020f0 (object 0x00000000d5f5e990, a java.lang.Object),
which is held by "Thread-1"
"Thread-1":
waiting to lock monitor 0x00007f8c2c002140 (object 0x00000000d5f5e9a0, a java.lang.Object),
which is held by "Thread-2"
Java stack information for the threads listed above:
===================================================
"Thread-2":
at com.example.DeadLock.lambda$main$1(DeadLock.java:32)
- waiting to lock <0x00000000d5f5e990> (a java.lang.Object)
- locked <0x00000000d5f5e9a0> (a java.lang.Object)
...
"Thread-1":
at com.example.DeadLock.lambda$main$0(DeadLock.java:20)
- waiting to lock <0x00000000d5f5e9a0> (a java.lang.Object)
- locked <0x00000000d5f5e990> (a java.lang.Object)
...
Found 1 deadlock.
方法2:JConsole可视化分析
1. 启动JConsole:
$ jconsole
2. 连接到进程:
选择本地进程或远程进程
3. 切换到"线程"标签
4. 点击"检测死锁"按钮
→ 自动显示死锁线程和锁信息 ✨
JConsole界面示例:
┌─────────────────────────────────────┐
│ JConsole - 线程监控 │
├─────────────────────────────────────┤
│ [检测死锁] ← 点这里 │
├─────────────────────────────────────┤
│ ⚠️ 发现死锁! │
│ │
│ Thread-1 (BLOCKED) │
│ 持有:lockA │
│ 等待:lockB (被Thread-2持有) │
│ │
│ Thread-2 (BLOCKED) │
│ 持有:lockB │
│ 等待:lockA (被Thread-1持有) │
└─────────────────────────────────────┘
方法3:VisualVM分析
1. 启动VisualVM:
$ jvisualvm
2. 连接进程
3. 线程 → 线程Dump
4. 自动高亮显示死锁线程(红色)
方法4:Arthas在线诊断(推荐!)🔥
# 1. 下载启动Arthas
curl -O https://arthas.aliyun.com/arthas-boot.jar
java -jar arthas-boot.jar
# 2. 选择进程
# 3. 查看线程
thread
# 输出示例:
Threads Total: 25, NEW: 0, RUNNABLE: 10, BLOCKED: 2, WAITING: 8, TIMED_WAITING: 5
ID NAME STATE CPU% TIME
-1 main RUNNABLE 0.0 0:0.234
12 Thread-1 BLOCKED 0.0 0:0.001 ← 死锁线程
13 Thread-2 BLOCKED 0.0 0:0.001 ← 死锁线程
# 4. 检测死锁
thread -b
# 输出:
"Thread-1" Id=12 BLOCKED on java.lang.Object@12345678 owned by "Thread-2" Id=13
"Thread-2" Id=13 BLOCKED on java.lang.Object@87654321 owned by "Thread-1" Id=12
# 5. 查看具体线程堆栈
thread 12
# 6. 查看所有BLOCKED线程
thread -state BLOCKED
四、死锁案例分析 🔬
案例1:转账死锁
public class TransferDeadlock {
public static class Account {
private int balance;
public void transfer(Account to, int amount) {
synchronized (this) { // 锁住转出账户
synchronized (to) { // 锁住转入账户
this.balance -= amount;
to.balance += amount;
}
}
}
}
public static void main(String[] args) {
Account a = new Account();
Account b = new Account();
// 线程1:A转给B
new Thread(() -> a.transfer(b, 100)).start();
// 线程2:B转给A
new Thread(() -> b.transfer(a, 200)).start();
// 💀 死锁!
// 线程1:持有A,等待B
// 线程2:持有B,等待A
}
}
解决方案1:按固定顺序加锁
public void transfer(Account to, int amount) {
Account first, second;
// 🔑 关键:按hashCode排序,保证加锁顺序一致
if (System.identityHashCode(this) < System.identityHashCode(to)) {
first = this;
second = to;
} else {
first = to;
second = this;
}
synchronized (first) {
synchronized (second) {
this.balance -= amount;
to.balance += amount;
}
}
}
解决方案2:使用tryLock超时
public boolean transfer(Account to, int amount) {
Lock fromLock = this.lock;
Lock toLock = to.lock;
try {
// 尝试获取两把锁,超时则放弃
if (fromLock.tryLock(1, TimeUnit.SECONDS)) {
try {
if (toLock.tryLock(1, TimeUnit.SECONDS)) {
try {
this.balance -= amount;
to.balance += amount;
return true;
} finally {
toLock.unlock();
}
}
} finally {
fromLock.unlock();
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return false; // 转账失败,可以重试
}
案例2:数据库连接池死锁
// ❌ 错误示例
public void processData() {
Connection conn1 = pool.getConnection(); // 获取连接1
try {
// 执行SQL1
conn1.executeQuery("SELECT ...");
// 又获取连接2(池子可能已满!)
Connection conn2 = pool.getConnection(); // 💀 可能死锁
try {
// 执行SQL2
conn2.executeQuery("INSERT ...");
} finally {
conn2.close();
}
} finally {
conn1.close();
}
}
// 场景:
// 线程1:持有conn1,等待conn2
// 线程2:持有conn2,等待conn3
// ...
// 线程10:持有conn10,等待conn1
// 连接池被耗尽,形成死锁!
解决方案:
// ✅ 正确:一个方法只用一个连接
public void processData() {
Connection conn = pool.getConnection();
try {
// 使用同一个连接
conn.executeQuery("SELECT ...");
conn.executeQuery("INSERT ...");
} finally {
conn.close();
}
}
// 或者:拆分成两个方法
public void method1() {
try (Connection conn = pool.getConnection()) {
conn.executeQuery("SELECT ...");
}
}
public void method2() {
try (Connection conn = pool.getConnection()) {
conn.executeQuery("INSERT ...");
}
}
案例3:读写锁降级死锁
// ❌ 错误:读锁升级为写锁(不支持!)
ReadWriteLock rwLock = new ReentrantReadWriteLock();
// 线程持有读锁,尝试获取写锁
rwLock.readLock().lock();
try {
// 读取数据
if (needUpdate) {
rwLock.writeLock().lock(); // 💀 死锁!读锁不能升级为写锁
try {
// 更新数据
} finally {
rwLock.writeLock().unlock();
}
}
} finally {
rwLock.readLock().unlock();
}
解决方案:
// ✅ 正确:支持写锁降级为读锁
rwLock.writeLock().lock(); // 先获取写锁
try {
// 更新数据
rwLock.readLock().lock(); // 降级:获取读锁
} finally {
rwLock.writeLock().unlock(); // 释放写锁
}
try {
// 读取数据
} finally {
rwLock.readLock().unlock(); // 释放读锁
}
五、预防死锁:编码规范 🛡️
原则1:破坏"持有并等待"
// ❌ 错误:持有锁的同时获取新锁
synchronized (lockA) {
doSomething();
synchronized (lockB) { // 持有lockA的同时获取lockB
doMore();
}
}
// ✅ 正确:一次性获取所有锁
// 方案1:使用工具类
public class LockManager {
public void executeWithLocks(Runnable task, Object... locks) {
// 按顺序获取所有锁
Arrays.sort(locks, Comparator.comparingInt(System::identityHashCode));
lockAll(locks);
try {
task.run();
} finally {
unlockAll(locks);
}
}
}
// 使用
lockManager.executeWithLocks(() -> {
// 业务代码
}, lockA, lockB, lockC);
原则2:破坏"循环等待"
// ✅ 所有线程按相同顺序获取锁
public class OrderedLock {
private static AtomicLong sequence = new AtomicLong(0);
private long order;
public OrderedLock() {
this.order = sequence.incrementAndGet();
}
public long getOrder() {
return order;
}
}
// 使用
public void transfer(Account to, int amount) {
OrderedLock first = this.lock.getOrder() < to.lock.getOrder() ? this.lock : to.lock;
OrderedLock second = this.lock.getOrder() < to.lock.getOrder() ? to.lock : this.lock;
synchronized (first) {
synchronized (second) {
// 转账逻辑
}
}
}
原则3:使用tryLock超时机制
// ✅ 尝试获取锁,超时则放弃
ReentrantLock lockA = new ReentrantLock();
ReentrantLock lockB = new ReentrantLock();
public boolean doWork() {
try {
if (lockA.tryLock(1, TimeUnit.SECONDS)) {
try {
if (lockB.tryLock(1, TimeUnit.SECONDS)) {
try {
// 业务逻辑
return true;
} finally {
lockB.unlock();
}
}
} finally {
lockA.unlock();
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return false; // 失败,可重试
}
原则4:减小锁的粒度
// ❌ 锁粒度太大
public synchronized void process() {
step1(); // 耗时操作
step2(); // 需要同步的操作
step3(); // 耗时操作
}
// ✅ 只锁必要的部分
public void process() {
step1(); // 不加锁
synchronized (this) {
step2(); // 只锁这里
}
step3(); // 不加锁
}
六、应急处理方案 🚑
方案1:重启服务(最简单粗暴)
# 1. 保存现场(线程dump)
jstack -l <pid> > deadlock_$(date +%Y%m%d_%H%M%S).txt
# 2. 重启服务
kill -9 <pid> # 强制杀死
./restart.sh # 重启
# 3. 观察是否复现
tail -f application.log
方案2:自动检测重启
// 定时检测死锁,自动重启
public class DeadlockMonitor {
private final ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
@Scheduled(fixedRate = 30000) // 每30秒检测一次
public void detectDeadlock() {
long[] deadlockedThreads = threadMXBean.findDeadlockedThreads();
if (deadlockedThreads != null && deadlockedThreads.length > 0) {
// 发现死锁!
ThreadInfo[] threadInfos = threadMXBean.getThreadInfo(deadlockedThreads);
// 记录日志
log.error("检测到死锁!涉及{}个线程", deadlockedThreads.length);
for (ThreadInfo info : threadInfos) {
log.error("死锁线程:{}", info);
}
// 发送告警
alertService.send("死锁告警", "系统检测到死锁,请立即处理!");
// 自动重启(谨慎使用!)
if (autoRestart) {
System.exit(1); // 退出,让进程管理器重启
}
}
}
}
方案3:熔断降级
// 检测到死锁风险时,熔断相关接口
@Service
public class TransferService {
private final CircuitBreaker circuitBreaker = CircuitBreaker.ofDefaults("transfer");
public void transfer(Account from, Account to, int amount) {
circuitBreaker.executeRunnable(() -> {
// 转账逻辑
doTransfer(from, to, amount);
});
}
// 熔断时的降级逻辑
public void transferFallback() {
throw new ServiceUnavailableException("转账服务暂时不可用,请稍后重试");
}
}
七、面试应答模板 🎤
面试官:线上发现死锁,如何快速定位和解决?
你的回答:
1. 症状识别(10秒)
- 接口大量超时
- 线程池耗尽
- CPU使用率低(线程都在等待)
2. 快速定位(1分钟内)
方法一:jstack
# 获取进程ID
jps -l
# 导出线程dump
jstack -l <pid> > dump.txt
# 查看末尾,jstack会自动检测死锁
tail -100 dump.txt
方法二:Arthas(推荐)
# 启动
java -jar arthas-boot.jar
# 检测死锁
thread -b
3. 分析死锁(看堆栈)
- 找到BLOCKED状态的线程
- 查看"waiting to lock"和"locked"信息
- 画出锁依赖图,找到循环
4. 应急处理(5分钟内)
- 保存线程dump现场
- 重启服务恢复业务
- 发送告警通知
5. 根本解决(后续优化)
- 按固定顺序加锁(破坏循环等待)
- 使用tryLock超时机制
- 减小锁粒度
- 代码Review,添加单元测试
举例: 曾经遇到转账死锁问题,两个线程互相转账:
- 线程1:A→B
- 线程2:B→A
通过jstack发现死锁后,改为按账户ID排序加锁,彻底解决了问题。
八、总结检查清单 ✅
开发阶段
- Code Review检查嵌套锁
- 统一加锁顺序规范
- 使用tryLock而非lock
- 单元测试覆盖并发场景
- 静态代码扫描(FindBugs、SpotBugs)
测试阶段
- 压力测试模拟高并发
- 长时间稳定性测试
- 混沌工程测试(随机延迟)
- 死锁检测工具监控
生产环境
- 定时死锁检测(30秒一次)
- 监控告警配置
- 线程dump自动保存
- 应急预案准备
- 降级开关配置
故障处理
- 保存现场(堆栈、日志)
- 快速恢复服务
- 分析根因
- 修复代码
- 复盘总结
记忆口诀:
死锁四条件,缺一不成立,
jstack是神器,thread -b来检测,
顺序加锁破循环,tryLock超时防等待,
线上死锁别慌张,重启保存再分析!🎵
核心要点:
- ✅ 死锁四要素:互斥、持有等待、不可剥夺、循环等待
- ✅ 定位工具:jstack、jconsole、Arthas
- ✅ 预防方法:固定顺序、tryLock、减小粒度
- ✅ 应急方案:保存现场→重启→分析→修复