Java CAS 专题:从 AtomicInteger 到乐观并发
CAS 是 Java 并发里非常核心的概念。
很多并发工具底层都会看到它的影子,比如:
AtomicInteger;AtomicLong;LongAdder;ConcurrentHashMap;- AQS;
- 乐观锁思想。
这篇把 CAS 从主线文章里单独拆出来讲,避免并发系列前几篇塞得太满。
主线文章可以先看:
锁相关内容看这里:Java 锁专题:从 synchronized 到分布式锁。
CAS 是什么
CAS 全称是 Compare And Swap,比较并交换。
它的核心思想是:
我认为当前值是 A如果内存里的值现在还是 A那就把它改成 B如果已经不是 A说明别人改过了,这次更新失败换成代码里的概念就是:
compareAndSet(expectedValue, newValue)只有当前值等于期望值时,才会更新成功。
为什么需要 CAS
先看一个普通计数器:
public class Counter { private int count = 0;
public void add() { count++; }}count++ 不是原子操作,它包含三个步骤:
读取 count加 1写回 count多个线程同时执行时,可能出现丢失更新。
使用 synchronized 可以解决,但它会让线程进入互斥等待。
CAS 的思路是不先加锁,而是更新时检查:
如果值没有被别人改过,我就更新;如果被改过,我就重试。
AtomicInteger
Java 里最常见的 CAS 工具是 AtomicInteger。
import java.util.concurrent.atomic.AtomicInteger;
public class Counter {
private final AtomicInteger count = new AtomicInteger(0);
public void add() { count.incrementAndGet(); }
public int get() { return count.get(); }}incrementAndGet() 底层会不断尝试 CAS。
大致逻辑可以理解成:
while (true) { int oldValue = get(); int newValue = oldValue + 1; if (compareAndSet(oldValue, newValue)) { return newValue; }}如果更新失败,说明期间有其他线程改过这个值,那就重新读取再试。
CAS 和 synchronized 的区别
两者都可以解决并发更新问题,但方式不同。
| 对比 | synchronized | CAS |
|---|---|---|
| 思路 | 加锁排队 | 不加锁,失败重试 |
| 阻塞 | 可能阻塞线程 | 通常不阻塞 |
| 适合场景 | 临界区较复杂 | 单个变量或简单状态更新 |
| 失败处理 | 线程等待锁 | CAS 失败后重试 |
CAS 更适合轻量、简单、冲突不太高的场景。
如果临界区里要做很多业务逻辑,或者需要同时保护多个变量,直接用 CAS 反而会让代码很难维护。
锁相关选择可以看:Java 锁专题:从 synchronized 到分布式锁。
CAS 的三个问题
CAS 不是万能的,常见问题有三个。
1. 自旋消耗 CPU
CAS 更新失败后通常会重试。
如果竞争很激烈,线程一直失败、一直重试,就会消耗 CPU。
所以 CAS 适合冲突不太高的场景。高冲突场景下,不一定比锁好。
2. 只能保证单个变量原子更新
CAS 很适合更新单个变量。
比如:
stock.decrementAndGet();但如果要同时维护多个字段:
库存减少销量增加状态变更写操作日志就不能简单依赖一个 CAS 解决全部一致性问题。
这种场景更适合事务、锁、状态机或数据库约束。
3. ABA 问题
ABA 问题是 CAS 的经典问题。
线程 A 读取到值是 1。
期间线程 B 把值从 1 改成 2,又改回 1。
线程 A 再 CAS 时发现值还是 1,就认为没有变过,于是更新成功。
但实际上这个值已经被别人改过两次。
A 读取:1B 修改:1 -> 2B 修改:2 -> 1A 判断:还是 1,CAS 成功如果业务只关心最终数值,ABA 可能没问题。
如果业务关心过程变化,ABA 就可能出问题。
解决思路是加版本号,比如 AtomicStampedReference。
AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 1);更新时不仅比较值,还比较版本号。
LongAdder
高并发计数场景下,AtomicLong 可能竞争很激烈。
LongAdder 的思路是把一个热点值拆散成多个分段,多个线程分别更新不同分段,最后求和。
LongAdder adder = new LongAdder();
adder.increment();adder.increment();
long count = adder.sum();适合:
- 访问量统计;
- QPS 统计;
- 监控计数;
- 只需要最终统计值的场景。
不适合:
- 要求每次读取都绝对精确;
- 要用当前值参与强一致业务判断;
- 库存扣减这类不能超卖的场景。
库存扣减应该优先使用数据库条件更新、乐观锁、Redis 预扣等业务方案。
CAS 和乐观锁的关系
CAS 和乐观锁思想很像。
CAS 是内存层面的比较并交换:
当前值 == 期望值 ? 更新 : 失败数据库乐观锁也是类似思想:
UPDATE productSET stock = stock - 1, version = version + 1WHERE id = #{productId} AND version = #{oldVersion};只有版本号没变,才说明这条数据没有被别人更新过。
区别在于:
- CAS 操作的是内存变量;
- 数据库乐观锁操作的是数据库记录;
- CAS 失败通常自旋重试;
- 数据库乐观锁失败要结合业务决定是否重试。
CAS 在并发集合里的作用
很多 JUC 工具内部都会使用 CAS。
比如 ConcurrentHashMap 在插入元素时,会在某些位置使用 CAS 尝试写入节点;竞争复杂时再配合锁控制。
这也是现代并发工具常见的设计方式:
低冲突:CAS 快速完成高冲突:退化到锁或其他控制方式所以不要把 CAS 和锁看成完全对立。
很多高质量并发工具,都是 CAS 和锁配合使用。
CAS 适合什么场景
适合:
- 单个变量计数;
- 状态从 A 改到 B;
- 冲突不高的轻量更新;
- 并发工具内部实现;
- 统计类数据累加。
不适合:
- 多字段强一致更新;
- 高冲突热点资源;
- 复杂业务临界区;
- 更新失败后不能简单重试;
- 需要跨 JVM 控制的场景。
比如商品库存扣减,如果只是单机内存变量,CAS 可以做 Demo。但真实电商库存通常在数据库或 Redis 里,需要结合业务一致性设计。
总结
CAS 的核心就是一句话:
先比较,再交换;值没变就更新,值变了就失败重试。
它的优势是轻量、不阻塞,适合简单变量更新。
它的限制是只能处理比较小的原子操作,高冲突下可能自旋消耗 CPU,也要注意 ABA 问题。
学习 CAS 时,重点不是记住某个 API,而是理解这种乐观并发思想。
后面看到 AtomicInteger、ConcurrentHashMap、AQS、乐观锁时,就能明白它们背后都有类似的判断逻辑:
我更新前先确认一下,数据还是不是我刚才看到的样子。If this article helped you, please share it with others!
Some information may be outdated






