一、什么是CAS?
想象你在快递柜前取快递:输入取件码后,系统会检查取件码是否正确(比如123456),如果匹配,柜门自动打开;如果不匹配,系统会拒绝操作 。CAS(Compare And Swap) 就是这个"检查-操作"
的原子过程,但它操作的是内存中的变量值。
- 操作原理 :当需要修改共享变量时,先比对
内存中的当前值(Current Value)与预期值(Expected Value)
:- 如果一致 → 原子性地更新为新值(New Value)
- 如果不一致 → 放弃操作或重试
- 原子性:整个过程不可分割,避免多线程竞争导致数据错乱
- 应用场景 :Java的
AtomicInteger
、无锁队列、自旋锁等并发控制
核心原理 :
CAS操作需要三个参数------内存地址V 、预期原值A 、新值B。
- 读取内存中的当前值V。
- 如果内存值V等于预期值A,则将V更新为新值B。
- 如果内存值V不等于预期值A,说明有其他线程修改过,放弃更新。
举个栗子 🌰:
两个线程同时给计数器count
加1:
- 线程A读取
count=0
,准备改为1。 - 线程B也读取
count=0
,准备改为1。 - 线程A的CAS操作成功,
count=1
。 - 线程B的CAS失败(此时
count=1≠0
),于是它重新读取count=1
,再次尝试更新为2。
最终,count=2
,整个过程无需加锁,实现高效并发。
CAS操作流程图
graph TD
A[开始] --> B[读取内存值V和预期值A]
B --> C{比较 V == A?}
C -->|是| D[更新为新值B]
D --> E[成功]
E --> F[结束]
C -->|否| G[自旋重试或放弃]
G --> |重新读取V| B
二、CAS的三大核心问题
-
ABA问题(幽灵修改)
- 案例一 :你的银行账户有100元
- 线程A读取余额为100,准备转账
- 线程B中途扣款100(余额=0),后又存入100(余额=100)
- 线程A执行CAS(预期值100→新值50),操作"成功"但实际中间发生过变动
- 案例二:朋友借走你的书,读完后又放回原处。虽然书还在原位,但内容可能被批注过。类似地,线程A读取值X,线程B将X→Y→X,当A执行CAS时,误认为数据未被修改。
- 风险1 :数据状态被隐形修改,可能导致逻辑错误
- 风险2 :在链表、栈等数据结构中尤为危险(如节点被删除后又恢复)。
- 案例一 :你的银行账户有100元
-
高竞争下的CPU性能消耗
- 当大量线程频繁CAS失败时,会导致CPU空转(类似反复刷新网页抢票)
- CAS失败时,线程会循环重试(自旋)。在高并发场景下,如果大量线程竞争同一个变量,CPU可能被长时间占用,性能急剧下降。
-
只能操作单个变量
- 只能保证一个共享变量的原子操作,复合操作需额外手段
- CAS无法保证多个变量的原子性更新。例如,转账需要同时修改两个账户的余额,单纯用CAS无法实现。
三、ABA问题的解决方案
方法1:版本号标记(类似快递单更新)
- 核心思想:每次修改给变量加上"版本号",比对值+版本号。
- 就像同一本书再版多次,内容可能一样,但版本号不同。CAS不仅要检查值,还要检查版本号是否匹配。
- Java实现 :
AtomicStampedReference(Java中的版本号原子类)
java
AtomicStampedReference<Integer> account = new AtomicStampedReference<>(100, 0);
// 转账操作
int oldStamp = account.getStamp();
int oldValue = account.getReference();
if (account.compareAndSet(oldValue, 50, oldStamp, oldStamp+1)) {
System.out.println("转账成功");
}
java
// 初始值:100,初始版本号:0
AtomicStampedReference<Integer> money = new AtomicStampedReference<>(100, 0);
// 读取当前值和版本号
int[] stampHolder = new int[1];
int oldMoney = money.get(stampHolder);
int oldStamp = stampHolder[0];
// 尝试更新值(110)和版本号(+1)
boolean success = money.compareAndSet(oldMoney, 110, oldStamp, oldStamp + 1);
流程对比:
普通CAS | 带版本号的CAS |
---|---|
检查值是否相等 | 检查值 和版本号 是否一致 |
值相同则更新 | 值且版本号均相同才更新 |
版本号的作用:
即使变量的值从A→B→A ,版本号也会从1→2→3,CAS操作能识别出中间的变化,彻底规避ABA问题。
方法2:时间戳记录(适合分布式系统)
- 每次修改记录时间戳,比对时间戳的先后顺序
graph LR
A[读取值+版本号] --> B{值&版本号匹配?}
B -->|是| C[更新值并增加版本号]
B -->|否| D[操作失败/重试]
四、应用技巧
-
优先使用带版本号的原子类
在Java中,涉及状态变化的场景(如金额、订单状态 )优先使用
AtomicStampedReference
。 -
避免滥用自旋
高并发时,若CAS竞争激烈,可结合锁或退避策略 (如CAS失败后随机等待,避免CPU风暴)减少CPU压力。
-
多变量原子操作
如需同时修改多个变量,可封装为对象,或用
AtomicReference
保证整体一致性。 -
降低竞争粒度 :如ConcurrentHashMap分段锁设计
-
LongAdder替代方案:Java8+针对高并发场景的优化类
五、总结
graph TD
CAS原理 -->|问题| ABA问题
CAS原理 -->|问题| 高竞争消耗
CAS原理 -->|问题| 单一变量限制
ABA问题 -->|解决方案| 版本号标记
ABA问题 -->|解决方案| 时间戳记录
CAS是无锁编程的利器,但ABA问题如同"程序世界的时光倒流",需用版本号精准识别数据变化。
- 简单场景:单变量无状态循环 → 直接用CAS。
- 复杂场景:涉及状态流转 → 必须加版本号或时间戳。
通过版本号等机制,我们可以让CAS在保持高性能的同时,具备识别幽灵操作的能力。理解这些原理,能帮助我们在实际开发中选择合适的并发控制策略。