CAS是CPU提供的原子指令,执行“读值—比对expected—相等则写入newValue”三步不可分割操作,无需阻塞即实现无锁并发;需循环重试、避免ABA问题,并非万能,适用单变量低冲突场景。

什么是CAS:不是“锁”,而是一次带条件的原子写入
CAS(CompareAndSwap)不是一种锁,也不是一个类或接口,它是CPU提供的一条原子指令(比如x86上的cmpxchg),Java通过Unsafe.compareAndSwapInt等本地方法把它暴露出来。你在代码里调用AtomicInteger.compareAndSet(expected, newValue),底层最终就是触发这条硬件指令。
它只做三件事:
① 读取内存中某个变量的当前值;
② 比较这个值是否等于你传入的expected(即你“以为它还是这个值”);
③ 如果相等,就把新值newValue写进去,并返回true;否则不写,返回false。
关键点在于:整个过程不可分割,不会被其他线程打断。所以它不需要阻塞线程,也不需要操作系统介入调度——这就是“无锁”的本质。
为什么expected不匹配就说明被改过了?
这不是靠魔法判断,而是靠你自己的使用逻辑保证的。你传的expected,必须是你上一次读到的值(比如先调get(),再拿这个结果当expected去compareAndSet)。如果中间有别的线程抢先修改了该变量,那么当前内存值就和你的expected对不上了。
立即学习“Java免费学习笔记(深入)”;
常见错误现象:
• 直接硬编码compareAndSet(0, 1),却没确认变量此时真是0
• 在两次调用之间插入了非原子操作(比如日志、网络请求),导致“读-判-写”窗口过大
• 忽略返回值,以为compareAndSet一定成功
正确做法:
• 把compareAndSet包在循环里,失败就重试(即“自旋”)
• 或者用更高层封装,比如incrementAndGet(),它内部已经帮你处理了重试逻辑
AtomicInteger怎么用才不踩坑?
AtomicInteger是CAS最典型的载体,但它不是万能计数器。它的适用场景很明确:单变量、简单变更(+1、set、getAndIncrement)、高并发低冲突。
容易被忽略的限制:
• 不支持复合操作,比如“如果大于10才加1”,不能靠一次CAS完成,得自己加循环+条件判断
• volatile语义只保可见性,不保原子性;但AtomicInteger所有更新方法都基于CAS,所以是线程安全的
• 如果自旋太频繁(比如几十万次),会白白消耗CPU,这时候不如用LongAdder(它用分段+Cell数组降低竞争)
public class Counter {
private final AtomicInteger count = new AtomicInteger();
public void safeIncrement() {
// 正确:失败自动重试,直到成功
while (true) {
int current = count.get();
int next = current + 1;
if (count.compareAndSet(current, next)) {
break;
}
// 可选:加个短暂yield()缓解CPU,但通常不需要
}
}
// 更推荐:直接用封装好的方法
public void preferredIncrement() {
count.incrementAndGet(); // 内部已做自旋,简洁且高效
}
}
ABA问题不是理论问题,是真实会出错的场景
CAS只看“值是否相同”,不管“有没有变过”。比如一个AtomicReference指向A对象,被线程1读取后,线程2把A→B→A又改回来了,此时线程1的CAS仍会成功——但它完全不知道中间发生过变化。
典型出错场景:
• 实现无锁栈时,头节点被弹出又压回,CAS误判为“没动过”
• 基于引用状态做业务判断(如“订单状态从‘待支付’变为‘已支付’”,但中间被取消又恢复)
解决办法:
• 用AtomicStampedReference,给引用带上版本戳(int stamp)
• 或者用AtomicMarkableReference,加一个boolean标记位
• 更彻底的方案:避免依赖“值未变”做业务决策,改用状态机+时间戳+唯一ID等更健壮的设计
CAS不是银弹。它高效的前提是竞争不激烈;它安全的前提是你理解并控制好了expected的来源;它简洁的前提是你没试图用它解决本该由锁或事务来管的问题。











