Skip to content
Java 并发与锁机制 · 第 3 篇 / 共 9 篇
领域编程与框架
专题Java 专题
当前序列Java 并发与锁机制
阅读位置第 3 篇 / 共 9 篇当前专题第 3 个序列 / 共 10 个序列

锁与 CAS 怎么选:synchronized、ReentrantLock、Atomic 和并发取舍

并发里最常见的误区之一,就是把“线程安全”直接等同于“加锁”。

实际上你面对的问题可能是:

  • 需要互斥
  • 需要原子更新
  • 需要可见性
  • 需要更细的并发控制

不同问题,对应的工具并不一样。

先说结论

可以先用这套判断:

  • 只要需要一段代码整体互斥,优先考虑 synchronized
  • 需要可中断、可超时、可尝试获取锁,再考虑 ReentrantLock
  • 只是做单变量原子更新,优先考虑 Atomic*LongAdder
  • CAS 不是“无敌更快”,高竞争场景下也可能很耗 CPU

一、什么是 CAS

CAS,常说成“比较并交换”。

它的核心思路是:

  1. 先读旧值
  2. 计算新值
  3. 提交时判断旧值是否还没变
  4. 没变就更新,变了就重试

这是一种典型的乐观并发思路。

二、CAS 适合什么场景

它特别适合:

  • 单个共享变量的原子更新
  • 冲突不是特别激烈
  • 希望减少阻塞和上下文切换

典型例子:

java
AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet();

三、CAS 的边界在哪里

CAS 很好用,但不是没有代价。

1. 自旋重试会消耗 CPU

如果竞争很激烈,线程会不断失败、不断重试。

这时虽然没有进入阻塞,但 CPU 压力可能会明显上升。

2. 更适合单变量原子更新

如果你要同时维护多个变量之间的一致性,只靠 CAS 往往不够。

3. 会有 ABA 问题

某个值从 A 变成 B,又变回 A,普通 CAS 可能察觉不到中间发生过变化。

这时可能需要:

  • 版本号
  • AtomicStampedReference

四、synchronized 什么时候用

它最适合的场景其实非常常见:

  • 一段代码需要完整互斥执行
  • 多个变量要一起保持一致
  • 逻辑清晰比技巧更重要

现在的 synchronized 早就不是早年印象里那种“性能一定很差”的代名词了。

在很多普通业务场景下,它依然是最稳、最直观的方案。

五、ReentrantLock 什么时候更合适

当你需要下面这些能力时,它会比 synchronized 更灵活:

  • tryLock():尝试获取锁
  • 可中断等待锁
  • 限时等待
  • 多条件队列 Condition
  • 更细的锁控制

例如:

java
if (lock.tryLock()) {
    try {
        // do something
    } finally {
        lock.unlock();
    }
}

但它也带来额外成本:

  • 代码更长
  • 更容易忘记释放锁
  • 可读性不一定更好

所以不是所有场景都值得上 ReentrantLock

六、AtomicInteger、LongAdder 怎么选

AtomicInteger

适合:

  • 普通计数
  • 并发不算特别激烈

LongAdder

适合:

  • 高并发计数
  • 写多读少

因为它通过分散热点减少竞争,吞吐通常会更好。

但如果你特别强调“每次读取都必须是全局精确瞬时值”,就要理解它的统计方式和一致性边界。

七、一个更实用的选择顺序

可以按这个顺序判断:

1. 是不是单变量原子更新

如果是:

  • AtomicInteger
  • AtomicLong
  • LongAdder

2. 是不是多变量或一整段逻辑都要互斥

如果是:

  • 先考虑 synchronized

3. 是否需要锁超时、可中断、多个条件队列

如果需要:

  • 再考虑 ReentrantLock

八、最常见的误区

1. 觉得 CAS 一定比锁高级

不是。它们只是解决问题的方式不同。

2. 复合逻辑想靠 Atomic 类一次解决

如果逻辑涉及多个状态协同更新,往往还是需要真正的同步控制。

3. 盲目追求“无锁”

无锁不是目标,可维护、可预测、能稳定跑才是目标。

一句话总结

锁和 CAS 不是谁替代谁,而是适用边界不同:

  • 单变量更新,CAS 很合适
  • 整段逻辑互斥,锁更直接
  • 普通业务先求正确,再谈更细的并发优化

先把问题类型分清楚,比先选工具重要得多。

延伸阅读相关文章优先当前专题,再补跨专题关联。
同一序列 · 顺着当前主线继续读AQS 到底解决了什么适合沿着 JUC、volatile、CAS、AQS 和锁实现这条主线往下看。Java 专题 · Java 并发与锁机制同一序列 · 顺着当前主线继续读ReentrantLock 实战怎么用适合沿着 JUC、volatile、CAS、AQS 和锁实现这条主线往下看。Java 专题 · Java 并发与锁机制同专题其他序列 · 共享标签:并发、选型对比线程池拒绝策略怎么选适合把线程池参数、拒绝策略、CompletableFuture 和 ForkJoin 放在一起连续看。Java 专题 · Java 线程池与异步编排同专题其他序列 · 共享标签:选型对比AtomicLong 和 LongAdder 怎么选适合把 synchronized、CAS、LongAdder 和并发容器的适用边界放到一起看。Java 专题 · Java 锁实现与并发细节跨专题关联 · 共享标签:并发、选型对比Redis 和 ZooKeeper 分布式锁怎么选适合把互斥控制、锁选型和协调边界单独放到一条协同主线上看,不再混进缓存治理序列。协同控制专题 · 协同控制与分布式锁跨专题关联 · 同场景:方案选型半同步复制和异步复制怎么选适合把 next-key lock、一致性读、复制延迟和分库分表边界放回同一条一致性主线上判断。MySQL 专题 · MySQL 事务、锁与高可用边界
继续阅读Java 并发与锁机制当前序列第 3 篇 / 共 9 篇当前专题第 3 个序列 / 共 10 个序列
往前看
上一篇volatile 的作用与边界回到当前序列上一章上一序列Java 集合与容器从第 1 篇开始:Java 集合怎么选
往后看
下一篇AQS 到底解决了什么继续当前序列下一章下一序列Java 线程池与异步编排从第 1 篇开始:线程池参数怎么配

把零散经验整理成可查、可复用、可持续更新的企业级知识门户