CAS是CPU提供的原子指令,Java通过Unsafe类封装并由AtomicInteger等原子类调用;失败时返回false而非抛异常,需正确重试;ABA问题可用AtomicStampedReference解决;高竞争或复杂逻辑时应选锁替代。

什么是CAS,它在Java里怎么被调用
CAS(Compare-And-Swap)不是Java语言关键字,而是底层CPU指令的封装,Java通过Unsafe类暴露了原始CAS能力,但日常开发中更常用的是java.util.concurrent.atomic包下的原子类,比如AtomicInteger、AtomicReference。
这些类的incrementAndGet()、compareAndSet(expected, updated)等方法,背后都调用了Unsafe.compareAndSwapInt()或其变体。JVM会将这些调用编译为平台相关的CAS汇编指令(如x86上的cmpxchg),保证单条指令的原子性。
注意:Unsafe本身是受限API,从JDK 9起默认不可反射访问,不建议直接使用;应优先走Atomic*类封装好的语义。
CAS失败时的典型表现和重试逻辑
当多个线程同时对同一个变量执行CAS操作,只有一个能成功,其余线程会拿到false返回值——这不是异常,而是CAS的正常行为。很多开发者误以为“失败=出错”,进而加锁兜底,反而破坏了无锁设计的初衷。
立即学习“Java免费学习笔记(深入)”;
常见错误写法是手动写死循环重试,却忽略了自旋开销和公平性问题:
while (!atomicRef.compareAndSet(oldVal, newVal)) {
oldVal = atomicRef.get(); // 忘记更新oldVal,导致无限循环
}
正确做法包括:
- 使用
getAndIncrement()、updateAndGet()等已内置重试逻辑的方法 - 若需自定义逻辑,务必在循环内重新读取当前值,例如
oldVal = atomicRef.get() - 高竞争场景下,考虑用
Thread.onSpinWait()(JDK 9+)提示CPU降低自旋功耗 - 极端情况(如长时自旋)可退化为
LockSupport.parkNanos(),但已脱离纯CAS范畴
ABA问题的真实影响与规避方式
ABA问题不是理论噱头:当一个值从A→B→A变化后,CAS会误判为“未被修改”。这在引用类型中尤其危险——比如AtomicReference指向的对象被回收又复用,可能导致内存安全漏洞或状态错乱。
标准解法是使用AtomicStampedReference或AtomicMarkableReference,它们把版本号或标记位和引用打包存储。关键点在于:
-
AtomicStampedReference.compareAndSet(expectedRef, newRef, expectedStamp, newStamp)必须同时校验引用和戳记 - 戳记不能简单用
int自增,要防止溢出回绕(可用Long或带防溢出检查的计数器) - 多数业务场景其实不需要处理ABA——比如计数器、状态标志位,值本身无历史含义,此时用基础
Atomic*更轻量
什么时候不该用CAS,以及替代方案
CAS不是银弹。当共享变量更新逻辑复杂(比如需同时修改多个字段)、或竞争极其激烈(实测自旋耗时 > 锁获取开销)时,CAS性能反而低于synchronized或ReentrantLock。
可通过JMH压测验证:在16核机器上,当线程数超过32且更新频率>10M ops/s时,AtomicInteger的吞吐可能反低于细粒度锁。
更实际的判断依据是:
- 更新是否满足“读-改-写”原子性且无副作用(如不依赖外部I/O或同步块)
- 是否需要阻塞等待(CAS只能忙等或放弃,无法挂起线程)
- 是否涉及复合操作(如“若为正则减1,否则设为0”)——这时应考虑
StampedLock乐观读 + 必要时升级写锁
真正容易被忽略的是伪共享(False Sharing):多个Atomic*变量在同一个CPU缓存行(64字节)里,会导致不必要的缓存失效。解决办法是用@Contended注解(需开启-XX:+UseContended)或手动填充字段对齐。











