🆘 线上死锁救援指南:从发现到解决的完整攻略!

94 阅读10分钟

一、什么是死锁?💀

经典比喻:两个人过独木桥

AB
      │                 │
   ┌──┴──┐           ┌──┴──┐
   │ 🚶‍♂️ │←─────────→│ 🚶‍♀️ │
   └─────┘           └─────┘
   
双方都不让步,谁也过不去!这就是死锁!

代码演示

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、减小粒度
  • ✅ 应急方案:保存现场→重启→分析→修复