Java多线程中的死锁问题与避免方法

死锁典型模式是多线程以不同顺序获取同一组锁,如线程A先锁accountA再锁accountB,线程B反之;常用tryLock()超时避免、统一锁顺序预防、jstack和ThreadMXBean排查。

死锁发生的典型代码模式

Java中死锁最常出现在多个线程以不同顺序获取同一组 Object 锁(或 synchronized 块)时。比如线程 A 先锁 accountA 再尝试锁 accountB,而线程 B 正好相反——这种交叉加锁是死锁的直接诱因。

常见错误现象包括:程序卡住无响应、CPU 占用率低、线程状态长期为 BLOCKED(可通过 jstack 查看线程堆栈,若看到两个以上线程互相等待对方持有的锁,基本可确认)。

  • 只在同步块内调用可能阻塞或依赖其他锁的方法,极易引入隐式锁依赖
  • 使用 ReentrantLock 时调用 lock() 而非 tryLock(long, TimeUnit),失去超时控制能力
  • 数据库事务 + JVM 锁混合使用时,JDBC 连接持有和对象锁顺序不一致,也会触发跨层死锁

如何用 tryLock() 主动规避死锁

ReentrantLock.tryLock(long, TimeUnit) 是最实用的防御手段——它允许你设定等待上限,失败后可释放已持锁并重试或回退,打破循环等待条件。

ReentrantLock lockA = new ReentrantLock();
ReentrantLock lockB = new ReentrantLock();

void transfer(Account from, Account to, BigDecimal amount) {
    while (true) {
        if (lockA.tryLock(100, TimeUnit.MILLISECONDS) &&
            lockB.tryLock(100, TimeUnit.MILLISECONDS)) {
            try {
                from.withdraw(amount);
                to.deposit(amount);
                return;
            } finally {
                lockA.unlock();
                lockB.unlock();
            }
        } else {
            // 至少一个锁没拿到,先释放已获得的锁,避免僵持
            if (lockA.isHeldByCurrentThread()) lockA.unlock();
            if (lockB.isHeldByCurrent

Thread()) lockB.unlock(); Thread.sleep(10); // 避免忙等 } } }

注意:tryLock() 不支持公平锁的“排队语义”,且必须严格配对 unlock(),否则会导致锁泄漏。

统一锁顺序是成本最低的预防方式

如果所有线程都按相同规则获取锁(例如总是先锁 ID 小的对象),就能从根源上消除循环等待。这不需要额外 API,只需约定和校验。

使用场景:账户转账、资源池分配、树形结构遍历等存在天然可比较标识的场景。

  • 对锁对象实现 Comparable,或提取唯一可比字段(如 account.getId()
  • 加锁前先排序:List locks = Arrays.asList(obj1, obj2); locks.sort(Comparator.comparing(System::identityHashCode)); —— 用 System.identityHashCode() 是安全兜底,但不保证跨 JVM 一致,仅限单 JVM 内有效
  • 避免在锁内做任何可能引发新锁竞争的操作(如调用外部服务、访问 synchronized 集合)

排查与监控不能只靠日志

死锁往往在压测或上线后才暴露,仅靠业务日志很难定位。必须结合 JVM 自带机制和轻量级埋点。

关键操作:

  • 启动参数加入 -XX:+PrintConcurrentLocks -XX:+PrintGCDetails,配合 jstack -l 可输出显式锁持有关系
  • 定期调用 java.lang.management.ThreadMXBean.findDeadlockedThreads() 做主动探测(建议封装为健康检查端点)
  • 对高风险同步块添加计时日志:long start = System.nanoTime(); ... log.warn("sync block took {}ms", (System.nanoTime()-start)/1_000_000);,持续偏高的耗时是潜在死锁前兆

真正难处理的是“锁链过长”:不是两把锁互等,而是 A→B→C→D→A 形成环路,这种靠人工 review 几乎不可控,必须依赖工具链自动分析锁获取路径。