死锁排查案例

wen java案例 2

本文目录导读:

  1. 死锁场景复现
  2. 死锁发生过程分析
  3. 死锁排查实战步骤
  4. 解决方案
  5. 排查要点总结

我来给你整理一份详细的死锁排查实战案例,从现象到解决手把手拆解。

死锁场景复现

典型业务场景

一个转账系统中,同时进行A→B和B→A的转账操作,就容易触发死锁。

public class TransferService {
    private static final Object lockA = new Object();
    private static final Object lockB = new Object();
    // 转账方法:from向to转账
    public static void transfer(String from, String to, int amount) {
        // 获取第一个账户的锁
        synchronized (getLock(from)) {
            System.out.println(Thread.currentThread().getName() 
                + " 获取到 " + from + " 的锁");
            // 模拟业务处理,增加死锁概率
            try { Thread.sleep(100); } catch (InterruptedException e) {}
            // 获取第二个账户的锁
            synchronized (getLock(to)) {
                System.out.println(Thread.currentThread().getName() 
                    + " 获取到 " + to + " 的锁");
                // 执行业务逻辑
                System.out.println(from + " 向 " + to + " 转账 " + amount);
            }
        }
    }
    private static Object getLock(String account) {
        return "A".equals(account) ? lockA : lockB;
    }
    public static void main(String[] args) {
        // 线程1:A→B转账
        new Thread(() -> transfer("A", "B", 100), "转账线程-1").start();
        // 线程2:B→A转账
        new Thread(() -> transfer("B", "A", 200), "转账线程-2").start();
    }
}

死锁发生过程分析

执行流程时序

线程-1                          线程-2
  |                               |
  |--获取lockA(成功)             |--获取lockB(成功)
  |--sleep(100ms)                 |--sleep(100ms)
  |                               |
  |--尝试获取lockB(等待...)      |--尝试获取lockA(等待...)
  |      ↑                        |      ↑
  |      └── 死锁!互相等待对方释放资源

死锁产生的四个必要条件

条件 状态 说明
互斥条件 ✅ 满足 锁只能被一个线程持有
持有并等待 ✅ 满足 持有自己的锁,还等待对方的锁
不可剥夺 ✅ 满足 锁只能由持有者自己释放
循环等待 ✅ 满足 线程1等线程2的锁,线程2等线程1的锁

死锁排查实战步骤

Step 1:找到问题进程

# 方法1:使用jps查找Java进程
$ jps -l
12345 com.example.TransferService
# 方法2:使用ps查找
$ ps -ef | grep java

Step 2:使用jstack分析线程栈

# 导出线程栈信息
$ jstack 12345 > thread_dump.txt
# 或者直接查看
$ jstack 12345

关键输出分析:

Found one Java-level deadlock:
=============================
"转账线程-2":
  waiting to lock monitor 0x0000000012345678 (object 0x00000000abcdef01, 
  a java.lang.Object),
  which is held by "转账线程-1"
"转账线程-1":
  waiting to lock monitor 0x0000000012345679 (object 0x00000000abcdef02, 
  a java.lang.Object),
  which is held by "转账线程-2"
Java stack information for the threads listed above:
===================================================
"转账线程-2":
    at com.example.TransferService.transfer(TransferService.java:24)
    - waiting to lock <0x00000000abcdef01> (a java.lang.Object)
    at com.example.TransferService.lambda$main$1(TransferService.java:43)
    at com.example.TransferService$$Lambda$1/0x00000008.run(...)
"转账线程-1":
    at com.example.TransferService.transfer(TransferService.java:24)
    - waiting to lock <0x00000000abcdef02> (a java.lang.Object)
    - locked <0x00000000abcdef01> (a java.lang.Object)
    at com.example.TransferService.lambda$main$0(TransferService.java:39)
    at com.example.TransferService$$Lambda$0/0x00000008.run(...)

Step 3:使用jconsole图形化工具排查

# 启动jconsole
$ jconsole 12345

操作路径: 线程 → 检测死锁

死锁排查案例

显示效果:

线程 "转账线程-1" 正在等待 "转账线程-2" 持有的锁
线程 "转账线程-2" 正在等待 "转账线程-1" 持有的锁

Step 4:使用VisualVM深入分析

# 启动VisualVM
$ visualvm

排查步骤:

  1. 在左侧选择目标进程
  2. 点击"线程"标签页
  3. 点击"线程Dump"按钮

线程Dump结果:

线程名称: 转账线程-1
状态: BLOCKED on ObjectMonitor
等待获取: 0x00000000abcdef02
已持有: 0x00000000abcdef01
堆栈:
    - TransferService.transfer(TransferService.java:24)
    - TransferService$$Lambda$0.run(...)
    - java.lang.Thread.run(Thread.java:748)
线程名称: 转账线程-2  
状态: BLOCKED on ObjectMonitor
等待获取: 0x00000000abcdef01
已持有: 0x00000000abcdef02
堆栈:
    - TransferService.transfer(TransferService.java:24)
    - TransferService$$Lambda$1.run(...)
    - java.lang.Thread.run(Thread.java:748)

解决方案

方案1:固定锁顺序(推荐)

public static void transfer(String from, String to, int amount) {
    // 根据账户名排序,保证锁获取顺序一致
    int fromHash = System.identityHashCode(from);
    int toHash = System.identityHashCode(to);
    Object firstLock = fromHash < toHash ? getLock(from) : getLock(to);
    Object secondLock = fromHash < toHash ? getLock(to) : getLock(from);
    synchronized (firstLock) {
        synchronized (secondLock) {
            // 业务逻辑
        }
    }
}

方案2:使用显式锁超时

import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.TimeUnit;
public class TransferService {
    private static final ReentrantLock lockA = new ReentrantLock();
    private static final ReentrantLock lockB = new ReentrantLock();
    public static boolean transfer(String from, String to, int amount) {
        ReentrantLock firstLock = getLock(from);
        ReentrantLock secondLock = getLock(to);
        try {
            // 尝试获取第一个锁,最多等待5秒
            if (!firstLock.tryLock(5, TimeUnit.SECONDS)) {
                return false;
            }
            try {
                // 尝试获取第二个锁,同样设置超时
                if (!secondLock.tryLock(5, TimeUnit.SECONDS)) {
                    return false;  // 获取失败,上层决定是否重试
                }
                try {
                    // 业务逻辑
                    return true;
                } finally {
                    secondLock.unlock();
                }
            } finally {
                firstLock.unlock();
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return false;
        }
    }
}

方案3:数据库层面的解决方案

对于数据库操作,可以使用数据库的锁机制和事务隔离级别来避免死锁。

排查要点总结

排查工具 使用场景 关键信息
jps 查看进程 进程ID
jstack 获取线程快照 锁的持有和等待关系
jconsole 实时监控 图形化死锁检测
VisualVM 深入分析 线程状态、锁详情
jcmd 轻量级命令 Thread.print指令
Arthas 在线调试 thread -n命令查看阻塞线程

排查口诀

一找进程  二抓快照  三看锁关系  四定位代码  五修复验证

死锁排查的核心是定位"谁持有谁的锁"以及"谁在等待谁",jstack是排查死锁最直接有效的工具。

抱歉,评论功能暂时关闭!