
本文探讨在多线程中安全交换两个共享对象值时,为何“反复尝试加锁”(spin-try-lock)不是推荐方案,并详解基于唯一锁序的确定性解决方案——通过全局一致的锁获取顺序彻底消除死锁风险。
本文探讨在多线程中安全交换两个共享对象值时,为何“反复尝试加锁”(spin-try-lock)不是推荐方案,并详解基于唯一锁序的确定性解决方案——通过全局一致的锁获取顺序彻底消除死锁风险。
在并发编程中,swapValue(Data other) 这类需同时操作两个可变对象的方法极易引发死锁。原始代码中直接对 this 和 other 使用 synchronized 方法调用,会导致线程 A 持有 a 锁等待 b,而线程 B 持有 b 锁等待 a,形成经典循环等待——即死锁的四个必要条件之一。
你提出的“自旋重试”方案看似可行:
private final Lock lock = new ReentrantLock();
public void swapValue(Data other) {
lock.lock();
while (!other.lock.tryLock()) {
lock.unlock();
lock.lock(); // 危险:可能无限循环或高CPU消耗
}
// ... 执行交换 ...
other.lock.unlock();
lock.unlock();
}但该方案存在严重缺陷:
- ❌ 非确定性与低效性:依赖竞争时机,线程可能长时间自旋,浪费 CPU 资源;
- ❌ 违反锁设计原则:ReentrantLock 应配合 try-finally 确保释放,而此处解锁逻辑耦合在业务路径中,易出错;
- ❌ 混合锁机制风险:getValue()/setValue() 仍使用 synchronized(即 intrinsic lock),与显式 Lock 混用会引入不可预测的锁竞争层次,加剧死锁可能性;
- ❌ 未解决根本问题:死锁源于锁获取顺序不一致,而非“锁是否可用”。
✅ 正确解法是:强制所有线程以相同、全局一致的顺序获取锁。核心思想是为每个 Data 实例赋予一个稳定、唯一且可比较的标识符,并约定:总是先获取 ID 小的对象锁,再获取 ID 大的对象锁。
由于 System.identityHashCode() 不保证唯一性(哈希冲突可能导致误判),推荐采用以下两种工业级实践之一:
方案一:使用 AtomicLong 全局序列号(推荐)
public class Data {
private static final AtomicLong NEXT_ID = new AtomicLong(0);
private final long id = NEXT_ID.getAndIncrement(); // 全局唯一、线程安全
private long value;
private final Lock lock = new ReentrantLock();
public Data(long value) {
this.value = value;
}
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 = first.getValue();
first.setValue(second.getValue());
second.setValue(temp);
} finally {
second.lock.unlock(); // 逆序释放:先 second 后 first
}
} finally {
first.lock.unlock();
}
}
}方案二:使用 UUID(适用于对象生命周期较长、ID 需跨 JVM 的场景)
private final UUID id = UUID.randomUUID(); // 全局唯一,但比较开销略高 // 后续锁序逻辑同上,用 id.compareTo(other.id) 判断顺序
关键注意事项:
- ✅ 锁序必须严格一致:无论调用方是 a.swapValue(b) 还是 b.swapValue(a),都按 id 升序锁定,从根本上打破循环等待;
- ✅ 释放顺序建议逆序:虽然本例中释放顺序不影响死锁,但按“后锁先释”(LIFO)是通用最佳实践,有助于资源及时释放和调试;
- ✅ 彻底移除 synchronized:显式 Lock 与 synchronized 混用会增加锁层级复杂度,应统一为显式锁管理;
- ⚠️ 避免 tryLock() 自旋:除非有明确超时与退避策略(如 tryLock(10, TimeUnit.MILLISECONDS) + 指数退避),否则纯 while(!tryLock()) 属反模式。
总结:死锁预防不应依赖概率性重试,而应通过确定性的锁获取协议实现。基于唯一 ID 的升序加锁策略简单、高效、可验证,是 Java 并发编程中处理多对象协作的黄金准则。










