
本文探讨在多线程环境下安全实现对象间值交换时,为何“反复尝试获取锁”(spin-locking)不是推荐方案,并系统介绍一种更可靠、可预测的防死锁策略——通过全局一致的锁获取顺序(如基于唯一id排序)来彻底消除死锁风险。
本文探讨在多线程环境下安全实现对象间值交换时,为何“反复尝试获取锁”(spin-locking)不是推荐方案,并系统介绍一种更可靠、可预测的防死锁策略——通过全局一致的锁获取顺序(如基于唯一id排序)来彻底消除死锁风险。
在并发编程中,swapValue(Data other) 这类跨对象操作极易引发死锁。原始代码中混合使用 synchronized 方法与显式 ReentrantLock,且未约定锁获取顺序,导致 a.swapValue(b) 与 b.swapValue(a) 可能以相反顺序竞争 a.lock/b.lock 和 a/b 的内置监视器锁——形成经典的循环等待,即死锁根源。
而您提出的“轮询重试”方案(while(!other.lock.tryLock()) { lock.unlock(); lock.lock(); })虽能偶然避免死锁,但存在严重缺陷:
- ❌ 非确定性:依赖线程调度时机和锁释放/获取的竞争窗口,行为不可预测;
- ❌ 资源浪费:空转(busy-waiting)消耗CPU,尤其在高争用场景下性能急剧下降;
- ❌ 违反锁设计原则:tryLock() 本意是支持超时或中断处理,而非构建自旋逻辑;
- ❌ 隐患叠加:仍与 synchronized 方法共存,实际锁粒度混乱(内置锁 + 显式锁),加剧不确定性。
✅ 推荐方案:全局一致的锁顺序(Lock Ordering)
核心思想:对任意两个 Data 实例,始终以相同、可比、不可变的顺序获取锁。这样可打破循环等待条件,从根源上杜绝死锁。
最常用且健壮的做法是引入一个全局唯一、不可变的标识符,并在加锁前按其自然序(如升序)统一排序:
public class Data {
private final long id; // 全局唯一ID,构造时生成
private volatile long value;
private final ReentrantLock lock = new ReentrantLock();
public Data(long value) {
this.id = generateUniqueId(); // 见下方实现
this.value = value;
}
// 推荐:使用 AtomicLong 保证全局唯一性(优于 System.identityHashCode)
private static final AtomicLong NEXT_ID = new AtomicLong(0);
private static long generateUniqueId() {
return NEXT_ID.incrementAndGet();
}
public long getValue() {
lock.lock();
try {
return value;
} finally {
lock.unlock();
}
}
public void setValue(long value) {
lock.lock();
try {
this.value = value;
} finally {
lock.unlock();
}
}
public void swapValue(Data other) {
// 关键:按 id 升序决定加锁顺序,确保全局一致
Data first = (this.id <= other.id) ? this : other;
Data second = (this.id <= other.id) ? other : this;
first.lock.lock();
try {
second.lock.lock();
try {
// 安全执行交换(此时两把锁均已持有)
long temp = this.getValue(); // 注意:此处调用已加锁的 getValue()
long newValue = other.getValue();
this.setValue(newValue);
other.setValue(temp);
} finally {
second.lock.unlock(); // 逆序释放:先 second,后 first
}
} finally {
first.lock.unlock();
}
}
}⚠️ 关键注意事项
- ID 必须真正唯一:System.identityHashCode() 仅是哈希值,存在碰撞可能,不可用于锁排序。应使用 AtomicLong 自增、UUID(字符串比较开销大)、或数据库序列等强唯一机制。
- 锁获取顺序严格一致:永远先锁 min(id1, id2),再锁 max(id1, id2)。即使 swapValue 被反向调用(b.swapValue(a)),计算出的 first/second 仍相同。
- 释放顺序建议逆序:虽然本例中释放顺序不影响死锁,但遵循“后锁先释”(LIFO)是通用最佳实践,有助于减少锁持有时间并提升可维护性。
- 移除 synchronized 混用:所有临界区统一通过 ReentrantLock 管理,避免内置锁与显式锁语义混淆。若需 synchronized 兼容性,应彻底重构为纯显式锁或纯内置锁方案。
- 考虑更高阶抽象:对于复杂协作场景,可进一步采用 java.util.concurrent.locks.StampedLock(读写优化)或 java.util.concurrent.locks.ReadWriteLock,但锁顺序原则依然适用。
总结
“反复尝试获取锁”是一种脆弱、低效、反模式的死锁规避手段。真正的工程实践应追求确定性与可证明安全性。通过为共享对象分配唯一ID并强制统一锁获取顺序,我们能以简洁、高效、无竞争的方式根除死锁风险。该模式已被广泛应用于数据库事务、分布式锁协调及高性能并发容器(如 ConcurrentHashMap 分段锁)中,是 Java 并发开发中值得牢固掌握的核心范式。










