Appearance
锁与 CAS 怎么选:synchronized、ReentrantLock、Atomic 和并发取舍
并发里最常见的误区之一,就是把“线程安全”直接等同于“加锁”。
实际上你面对的问题可能是:
- 需要互斥
- 需要原子更新
- 需要可见性
- 需要更细的并发控制
不同问题,对应的工具并不一样。
先说结论
可以先用这套判断:
- 只要需要一段代码整体互斥,优先考虑
synchronized - 需要可中断、可超时、可尝试获取锁,再考虑
ReentrantLock - 只是做单变量原子更新,优先考虑
Atomic*或LongAdder - CAS 不是“无敌更快”,高竞争场景下也可能很耗 CPU
一、什么是 CAS
CAS,常说成“比较并交换”。
它的核心思路是:
- 先读旧值
- 计算新值
- 提交时判断旧值是否还没变
- 没变就更新,变了就重试
这是一种典型的乐观并发思路。
二、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. 是不是单变量原子更新
如果是:
AtomicIntegerAtomicLongLongAdder
2. 是不是多变量或一整段逻辑都要互斥
如果是:
- 先考虑
synchronized
3. 是否需要锁超时、可中断、多个条件队列
如果需要:
- 再考虑
ReentrantLock
八、最常见的误区
1. 觉得 CAS 一定比锁高级
不是。它们只是解决问题的方式不同。
2. 复合逻辑想靠 Atomic 类一次解决
如果逻辑涉及多个状态协同更新,往往还是需要真正的同步控制。
3. 盲目追求“无锁”
无锁不是目标,可维护、可预测、能稳定跑才是目标。
一句话总结
锁和 CAS 不是谁替代谁,而是适用边界不同:
- 单变量更新,CAS 很合适
- 整段逻辑互斥,锁更直接
- 普通业务先求正确,再谈更细的并发优化
先把问题类型分清楚,比先选工具重要得多。