跳到主要内容

锁与无锁方案的成本比较

锁和无锁算法都在协调共享状态。选择时要比较临界区长度、竞争强度、失败重试、调度延迟与正确性复杂度,不能把“没有阻塞”直接等同于“性能更高”。

1. 锁把失败竞争交给等待协议

lock.lock();
try {
updateState();
} finally {
lock.unlock();
}

低竞争时锁可以走很短的获取路径;竞争持续时,等待线程可能自旋一段时间或被挂起,释放时再唤醒。成本包括原子指令、缓存迁移、排队和上下文切换。

锁的优势是能自然覆盖多个字段和多步不变量,并能配合条件队列等待状态变化。

2. 无锁更新把竞争变成重试

while (true) {
State current = state.get();
State next = current.advance();
if (state.compareAndSet(current, next)) {
return;
}
}

CAS 失败时线程无需等待锁所有者,但之前的计算可能作废。竞争越高,失败重试和缓存行抖动越明显;如果 advance() 很贵,浪费更大。

完整算法还要处理 ABA、节点回收和进度保证。一个使用 CAS 的循环不自动成为正确的 lock-free 数据结构。

3. 两类方案各有容易出错的方式

方案常见风险
死锁、锁顺序错误、持锁 I/O、优先级反转、忘记释放
CAS / 无锁活锁式重试、饥饿、ABA、副作用重复、算法难以证明

可维护性也是成本。一个容易审查的短临界区,往往比自制的无锁链表更适合业务代码。

4. 先改变竞争形态

热点状态不一定要在两类方案中二选一:

  • 将计数分片,再周期聚合。
  • 按 key 划分所有权,减少线程争夺同一数据。
  • 批量提交,摊薄同步次数。
  • 发布不可变快照,让读取不参与写锁竞争。
  • 使用成熟并发容器,不重复实现底层算法。

减少共享通常比替换同步原语带来更稳定的收益。

5. 基准需要覆盖竞争曲线

至少测试 1、2、核心数以及超额订阅等多个线程数,并分别记录:

  • 成功操作吞吐和 p95/p99 延迟。
  • CAS 失败、锁等待与 park 时间。
  • CPU 使用、上下文切换和分配率。
  • 不同读写比例、临界区长度和 key 分布。

JMH 负责建立可靠的微基准框架,JFR 或 async-profiler 用来解释结果。只测单线程无法回答并发方案的扩展性。

6. 常见问题

6.1 自旋锁适合很短的临界区吗

只有在等待预计很短、CPU 没有严重超额订阅并且竞争可控时才可能合适。业务代码通常不应自行实现自旋锁,JVM 和 JUC 已能根据场景处理大量细节。

6.2 公平锁会更快吗

公平性通常减少插队,却增加排队与唤醒约束,吞吐往往更低。只有确实需要限制长期等待时才承担这项成本。

7. 面试题

7.1 为什么无锁方案在高竞争下可能比锁更慢

出现公司:阿里巴巴、美团

考察重点

  • CAS 失败重试与缓存行迁移。
  • 阻塞成本和临界区长度。
  • 用竞争曲线而不是标签做判断。

相关内容:第 1 节“锁把失败竞争交给等待协议”至第 5 节“基准需要覆盖竞争曲线”。

参考回答

无锁更新失败后要重新读取和计算,高竞争时许多线程会反复争夺同一缓存行,CPU 用在失败重试而不是业务进展上。锁可以让失败者排队或挂起,若临界区清楚且竞争持续,反而可能更稳定。

应按线程数、读写比例和临界区成本压测吞吐与长尾,再用 CAS 失败、锁等待、CPU 和切换数据解释结果。还要把算法复杂度和正确性风险算入选择。