cas在x86上对应cmpxchg(带lock前缀),armv8上为ldaxr/stlxr成对指令;其失败仅因current≠expected,属乐观并发设计,并非指令不原子。

CAS在Java里到底调用的是哪个CPU指令
Java的AtomicInteger.compareAndSet()这类操作,最终落到x86上基本就是cmpxchg指令(带lock前缀),ARMv8则是ldaxr/stlxr成对使用。JVM不是自己拼汇编,而是通过Unsafe.compareAndSwapInt()委托给HotSpot的intrinsics——这些是JVM预置的、针对不同CPU架构优化过的原生实现。
常见错误现象:在非volatile字段上手动“模拟CAS”(比如先读再if判断再写),结果被JIT优化掉重排序,或因缓存不一致导致失败。这不是CAS的问题,是没理解它必须配合内存屏障和可见性语义。
-
compareAndSet()成功时隐含一个full memory barrier,失败时不保证任何顺序约束 - 不要试图用
synchronized块+普通变量“复刻”CAS逻辑——锁开销大,且无法提供无锁的乐观并发语义 - JDK 9+中
VarHandle.compareAndSet()已逐步替代Unsafe,但底层指令映射关系不变
为什么CAS会失败?不是原子的吗
CAS本身是CPU级原子指令,但“失败”指比较阶段发现当前值≠预期值,于是不执行交换。这不等于指令出错,而是设计使然——它本质是乐观策略:假设冲突少,失败了由上层重试。
典型场景:多个线程同时对AtomicInteger做incrementAndGet(),其中某次CAS因其他线程抢先修改而失败,然后循环重试直到成功。
立即学习“Java免费学习笔记(深入)”;
- 失败原因只有且仅有一个:
current value != expected value,不会因为内存对齐、字长不对齐或权限问题失败 - 32位变量在64位JVM上通常没问题,但跨平台代码要小心
long/double在32位JVM上可能被拆成两次32位操作(不过HotSpot已保证其原子性) - 如果频繁失败(如高争用场景),会导致CPU空转、伪共享加剧,这时该考虑分段计数(如
LongAdder)而非死磕单个CAS
Unsafe.compareAndSwapXxx()和VarHandle.compareAndSet()的区别在哪
两者语义一致,但Unsafe是内部API,无强兼容保证;VarHandle是JDK 9引入的标准替代方案,类型安全、支持泛型、可反射获取,且JVM对其做了同等深度的intrinsics优化。
错误用法:在JDK 11+项目里还硬引Unsafe.getUnsafe(),不仅容易触发IllegalAccessError,而且绕过模块系统限制。
-
VarHandle需通过MethodHandles.lookup().findStaticVarHandle()或lookup.findVarHandle()获取,不能直接new - 参数顺序一致:
compareAndSet(obj, expected, desired),但Unsafe多一个offset参数(compareAndSwapInt(obj, offset, expected, desired)) - 性能无差异——HotSpot对二者生成的汇编几乎完全相同,别为“更底层”迷信
Unsafe
CAS能保证ABA问题吗
不能。CAS只比对值是否相等,不关心变化路径。A→B→A这种情形下,CAS会误认为“没变过”,从而允许错误更新。
典型场景:无锁栈/队列中节点被弹出、回收、重用,又恰好分配到同一内存地址,此时CAS可能把旧状态当新状态接受。
-
AtomicStampedReference用版本号戳破ABA,但增加一次额外的int字段读写开销 -
AtomicMarkableReference只区分“标记/未标记”,适合只需二元状态的场景 - 很多情况下ABA根本无害(比如计数器、状态机中的枚举切换),强行加stamp反而拖慢正常路径
- 现代GC(如ZGC、Shenandoah)的内存管理让对象重用地址的概率大幅降低,ABA实际发生率比教科书描述低得多
真正难处理的从来不是CAS指令本身,而是如何定义“什么才算一次有效变更”——这得看业务逻辑,CPU不管这个。









