大家好,我是桦说编程。
死锁是多线程编程的噩梦,一旦发生程序就会挂起。本文介绍 Guava 的
CycleDetectingLockFactory,让你在开发阶段就能发现潜在死锁,而不是等到生产环境暴雷。
问题背景:死锁焦虑
使用 ReentrantLock 或 synchronized 时,最担心的就是死锁:
java
// 线程1:先锁A,再锁B
lockA.lock();
lockB.lock();
// 线程2:先锁B,再锁A
lockB.lock();
lockA.lock();
这种代码在单元测试中可能运行正常,但在生产环境高并发时突然死锁,程序挂起,只能重启。
核心痛点:
- 死锁难以复现,测试阶段很难发现
- 一旦发生,只能强制重启
- 排查需要线程 dump 分析,耗时耗力
CycleDetectingLockFactory:运行时死锁检测
Guava 提供的 CycleDetectingLockFactory 通过构建锁的依赖图,在获取锁时实时检测是否会形成环路(死锁)。
核心原理
- 锁依赖图:记录每个线程获取锁的顺序
- 环路检测:尝试获取新锁时,检查是否会形成环路
- 即时失败 :检测到潜在死锁时,立即抛出
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 的优势
- 即时反馈:开发阶段就能发现潜在死锁
- 零侵入:只需替换锁的创建方式,业务代码不变
- 可配置:根据环境选择不同策略(抛异常/告警/禁用)
- 清晰诊断:异常信息包含完整的锁依赖链
使用场景
- 推荐使用:多个锁、加锁顺序不确定的场景
- 测试环境:全局使用 THROW 策略,自动化测试发现问题
- 生产环境:使用 WARN 策略作为监控手段
- 不推荐:单锁场景、加锁顺序固定的场景(无必要)
总结
- 死锁难以复现 ,生产环境暴露成本高,
CycleDetectingLockFactory让问题前移 - THROW 策略 用于测试环境,WARN 策略用于生产环境
- 死锁检测是兜底,根本方案是设计合理的加锁顺序
- 零侵入改造,只需替换锁的创建方式,立即获得死锁检测能力
- 检测开销:需要维护锁依赖图,有轻微性能损耗
使用 CycleDetectingLockFactory 可以让你摆脱死锁焦虑,在开发阶段就发现问题,而不是等到生产环境暴雷后手忙脚乱。
如果这篇文章对你有帮助,欢迎关注我,持续分享高质量技术干货,助你更快提升编程能力。