并发编程高级技巧:运行时检测死锁,告别死锁焦虑

大家好,我是桦说编程。

死锁是多线程编程的噩梦,一旦发生程序就会挂起。本文介绍 Guava 的 CycleDetectingLockFactory,让你在开发阶段就能发现潜在死锁,而不是等到生产环境暴雷。

问题背景:死锁焦虑

使用 ReentrantLocksynchronized 时,最担心的就是死锁:

java 复制代码
// 线程1:先锁A,再锁B
lockA.lock();
lockB.lock();

// 线程2:先锁B,再锁A
lockB.lock();
lockA.lock();

这种代码在单元测试中可能运行正常,但在生产环境高并发时突然死锁,程序挂起,只能重启。

核心痛点

  • 死锁难以复现,测试阶段很难发现
  • 一旦发生,只能强制重启
  • 排查需要线程 dump 分析,耗时耗力

CycleDetectingLockFactory:运行时死锁检测

Guava 提供的 CycleDetectingLockFactory 通过构建锁的依赖图,在获取锁时实时检测是否会形成环路(死锁)。

核心原理

  1. 锁依赖图:记录每个线程获取锁的顺序
  2. 环路检测:尝试获取新锁时,检查是否会形成环路
  3. 即时失败 :检测到潜在死锁时,立即抛出 PotentialDeadlockException

三种检测策略

java 复制代码
// 1. THROW - 抛出异常(推荐用于开发/测试环境)
CycleDetectingLockFactory.newInstance(Policies.THROW);

// 2. WARN - 打印警告日志(适合生产环境)
CycleDetectingLockFactory.newInstance(Policies.WARN);

// 3. DISABLED - 禁用检测(性能敏感场景)
CycleDetectingLockFactory.newInstance(Policies.DISABLED);

对比演示:死锁 vs 死锁检测

场景:转账系统

两个账户相互转账,线程1从A转到B,线程2从B转到A。

死锁版本(ReentrantLock)

使用普通 ReentrantLock,程序会挂起:

java 复制代码
static class Account {
    private final ReentrantLock lock = new ReentrantLock();

    public void transfer(Account target, int amount) {
        lock.lock();  // 先锁自己
        try {
            target.lock.lock();  // 再锁对方 - 可能死锁!
            try {
                // 转账逻辑
            } finally {
                target.lock.unlock();
            }
        } finally {
            lock.unlock();
        }
    }
}

运行结果:

css 复制代码
线程1 获取了 账户A 的锁
线程2 获取了 账户B 的锁
=== 3秒后程序仍未结束,发生死锁 ===
线程1状态: WAITING
线程2状态: WAITING

死锁检测版本(CycleDetectingLockFactory)

替换为 CycleDetectingLockFactory,死锁会被立即检测:

java 复制代码
private static final CycleDetectingLockFactory lockFactory =
    CycleDetectingLockFactory.newInstance(Policies.THROW);

static class Account {
    private final Lock lock;

    public Account(String name, int balance) {
        this.lock = lockFactory.newReentrantLock(name);  // 使用工厂创建锁
    }

    public void transfer(Account target, int amount) {
        lock.lock();
        try {
            target.lock.lock();  // 检测到死锁会抛出异常
            try {
                // 转账逻辑
            } finally {
                target.lock.unlock();
            }
        } catch (PotentialDeadlockException e) {
            System.err.println("检测到潜在死锁: " + e.getMessage());
        } finally {
            lock.unlock();
        }
    }
}

运行结果:

css 复制代码
线程1 获取了 账户A 的锁
线程2 获取了 账户B 的锁
线程2 检测到潜在死锁!
死锁信息: Potential Deadlock from LockGraphNode{账户B} to LockGraphNode{账户A}
=== 程序正常结束,未发生死锁挂起 ===

实战建议

1. 开发/测试环境使用 THROW 策略

java 复制代码
// 在测试环境配置
private static final CycleDetectingLockFactory lockFactory =
    CycleDetectingLockFactory.newInstance(Policies.THROW);

好处:单元测试、集成测试中自动发现死锁问题,快速失败。

2. 生产环境使用 WARN 策略

java 复制代码
// 在生产环境配置
private static final CycleDetectingLockFactory lockFactory =
    CycleDetectingLockFactory.newInstance(Policies.WARN);

好处:记录告警日志,但不中断业务,便于后续优化。

3. 性能敏感场景可以禁用

java 复制代码
// 性能优先场景
private static final CycleDetectingLockFactory lockFactory =
    CycleDetectingLockFactory.newInstance(Policies.DISABLED);

注意:只有在确认没有死锁风险,且性能测试证明检测有明显开销时才禁用。

4. 更好的方案:固定加锁顺序

死锁检测只是兜底手段,根本解决方案是避免环路依赖

java 复制代码
// 按照固定顺序加锁,避免死锁
private static void safeTransfer(Account from, Account to, int amount) {
    Account first = System.identityHashCode(from) < System.identityHashCode(to) ? from : to;
    Account second = first == from ? to : from;

    first.lock.lock();
    try {
        second.lock.lock();
        try {
            // 转账逻辑
        } finally {
            second.lock.unlock();
        }
    } finally {
        first.lock.unlock();
    }
}

完整示例

java 复制代码
/**
 * CycleDetectingLockFactory 死锁检测演示
 *
 * <p>与 DeadlockDemo 相同的转账场景,但使用 CycleDetectingLockFactory
 * <p>当检测到潜在死锁时,会立即抛出 PotentialDeadlockException,而不是挂起程序
 */
public class CycleDetectingLockDemo {

    // 创建锁工厂,THROW 策略会在检测到死锁时抛出异常
    private static final CycleDetectingLockFactory lockFactory =
            CycleDetectingLockFactory.newInstance(CycleDetectingLockFactory.Policies.THROW);

    static class Account {
        private final String name;
        private final Lock lock;
        private int balance;

        public Account(String name, int balance) {
            this.name = name;
            this.balance = balance;
            // 使用 CycleDetectingLockFactory 创建锁
            // 每个锁都有一个唯一的标识,用于构建锁的依赖图
            this.lock = lockFactory.newReentrantLock(name);
        }

        public void transfer(Account target, int amount) {
            try {
                // 先锁定自己的账户
                lock.lock();
                try {
                    System.out.println(Thread.currentThread().getName() +
                            " 获取了 " + name + " 的锁");

                    // 模拟业务处理耗时
                    Thread.sleep(100);

                    // 尝试锁定目标账户
                    // 如果会形成循环依赖(死锁),这里会立即抛出 PotentialDeadlockException
                    target.lock.lock();
                    try {
                        System.out.println(Thread.currentThread().getName() +
                                " 获取了 " + target.name + " 的锁");

                        this.balance -= amount;
                        target.balance += amount;

                        System.out.println(Thread.currentThread().getName() +
                                " 完成转账: " + name + " -> " + target.name + " ¥" + amount);
                    } finally {
                        target.lock.unlock();
                    }
                } finally {
                    lock.unlock();
                }
            } catch (CycleDetectingLockFactory.PotentialDeadlockException e) {
                // 检测到潜在死锁,立即抛出异常
                System.err.println(Thread.currentThread().getName() +
                        " 检测到潜在死锁!");
                System.err.println("死锁信息: " + e.getMessage());
                // 打印完整的锁依赖链
                e.printStackTrace();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }

        @Override
        public String toString() {
            return name + ": ¥" + balance;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Account accountA = new Account("账户A", 1000);
        Account accountB = new Account("账户B", 1000);

        System.out.println("=== CycleDetectingLockFactory 死锁检测演示 ===");
        System.out.println("初始状态: " + accountA + ", " + accountB);
        System.out.println();

        // 线程1: A -> B 转账
        Thread thread1 = new Thread(() -> {
            accountA.transfer(accountB, 200);
        }, "线程1");

        // 线程2: B -> A 转账
        Thread thread2 = new Thread(() -> {
            accountB.transfer(accountA, 300);
        }, "线程2");

        thread1.start();
        thread2.start();

        // 等待线程结束
        thread1.join(5000);
        thread2.join(5000);

        System.out.println();
        System.out.println("=== 程序正常结束,未发生死锁挂起 ===");
        System.out.println("最终状态: " + accountA + ", " + accountB);
        System.out.println("线程1状态: " + thread1.getState());
        System.out.println("线程2状态: " + thread2.getState());
    }

    /**
     * 演示正确的锁顺序 - 避免死锁
     */
    public static class SafeTransferDemo {
        public static void main(String[] args) throws InterruptedException {
            Account accountA = new Account("账户A", 1000);
            Account accountB = new Account("账户B", 1000);

            System.out.println("=== 安全转账演示(按固定顺序加锁)===");
            System.out.println("初始状态: " + accountA + ", " + accountB);
            System.out.println();

            // 两个线程都按照相同的顺序加锁:先 A 后 B
            Thread thread1 = new Thread(() -> {
                safeTransfer(accountA, accountB, 200);
            }, "线程1");

            Thread thread2 = new Thread(() -> {
                safeTransfer(accountB, accountA, 300);
            }, "线程2");

            thread1.start();
            thread2.start();

            thread1.join();
            thread2.join();

            System.out.println();
            System.out.println("=== 安全转账完成 ===");
            System.out.println("最终状态: " + accountA + ", " + accountB);
        }

        private static void safeTransfer(Account from, Account to, int amount) {
            // 按照固定顺序加锁:使用 hashCode 或其他标识符确定顺序
            Account first = System.identityHashCode(from) < System.identityHashCode(to) ? from : to;
            Account second = first == from ? to : from;

            try {
                first.lock.lock();
                try {
                    System.out.println(Thread.currentThread().getName() +
                            " 获取了 " + first.name + " 的锁");

                    Thread.sleep(100);

                    second.lock.lock();
                    try {
                        System.out.println(Thread.currentThread().getName() +
                                " 获取了 " + second.name + " 的锁");

                        from.balance -= amount;
                        to.balance += amount;

                        System.out.println(Thread.currentThread().getName() +
                                " 完成转账: " + from.name + " -> " + to.name + " ¥" + amount);
                    } finally {
                        second.lock.unlock();
                    }
                } finally {
                    first.lock.unlock();
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }
}

核心要点

CycleDetectingLockFactory 的优势

  1. 即时反馈:开发阶段就能发现潜在死锁
  2. 零侵入:只需替换锁的创建方式,业务代码不变
  3. 可配置:根据环境选择不同策略(抛异常/告警/禁用)
  4. 清晰诊断:异常信息包含完整的锁依赖链

使用场景

  • 推荐使用:多个锁、加锁顺序不确定的场景
  • 测试环境:全局使用 THROW 策略,自动化测试发现问题
  • 生产环境:使用 WARN 策略作为监控手段
  • 不推荐:单锁场景、加锁顺序固定的场景(无必要)

总结

  • 死锁难以复现 ,生产环境暴露成本高,CycleDetectingLockFactory 让问题前移
  • THROW 策略 用于测试环境,WARN 策略用于生产环境
  • 死锁检测是兜底,根本方案是设计合理的加锁顺序
  • 零侵入改造,只需替换锁的创建方式,立即获得死锁检测能力
  • 检测开销:需要维护锁依赖图,有轻微性能损耗

使用 CycleDetectingLockFactory 可以让你摆脱死锁焦虑,在开发阶段就发现问题,而不是等到生产环境暴雷后手忙脚乱。


如果这篇文章对你有帮助,欢迎关注我,持续分享高质量技术干货,助你更快提升编程能力。

相关推荐
jiayong232 小时前
Spring AI Alibaba 深度解析(三):实战示例与最佳实践
java·人工智能·spring
梁同学与Android2 小时前
Android ---【经验篇】ArrayList vs CopyOnWriteArrayList 核心区别,怎么选择?
android·java·开发语言
无限大62 小时前
为什么"软件测试"很重要?——从 Bug 到高质量软件的保障
后端
ss2732 小时前
从零实现线程池:自定义线程池的工作线程设计与实现
java·开发语言·jvm
苗壮.2 小时前
CommandLineRunner 是什么?
java
石工记2 小时前
windows 10直接安装多个JDK
java·开发语言
菜鸟233号2 小时前
力扣669 修剪二叉搜索树 java实现
java·数据结构·算法·leetcode
健康平安的活着3 小时前
springboot+sse的实现案例
java·spring boot·后端