CAS在Java中是基于ARM的LL/SC机制实现的逻辑语义,通过LDREX/STREX循环重试完成原子操作,并需JVM插入DMB等内存屏障以满足JMM顺序一致性要求。

CAS 在 Java 中并不是一条 CPU 指令,而是一个逻辑语义:比较内存值是否等于预期值,相等则原子更新。在 ARM 架构上,JVM(如 HotSpot)底层不直接调用“CAS 指令”,而是借助硬件提供的 LL/SC(Load-Linked / Store-Conditional)机制 来实现这一语义——这是 ARMv7/v8 的标准原子原语,也是 Java AtomicInteger.compareAndSet() 等方法的真正执行基础。
LL/SC 是怎么工作的?
ARM 不提供类似 x86 的 CMPXCHG 单指令 CAS,而是拆成两步协同完成:
- LDREX(Load-Exclusive):从指定地址读取当前值,并在 CPU 内部设置一个“独占监视标记”(exclusive monitor),同时记录该地址。这个标记是 per-core、per-address 的,且不可见、不可编程。
- STREX(Store-Exclusive):尝试将新值写回同一地址。仅当自 LDREX 后,该地址未被任何其他处理器或本核的其他指令修改过(即 exclusive monitor 仍有效),写入才成功,返回 0;否则失败,返回 1。
整个过程由硬件保证“读–改–写”(RMW)的原子性边界,但不是单条指令原子——它依赖程序员(或 JVM)用循环重试来封装语义。
JVM 如何把 compareAndSet 编译成 LL/SC?
HotSpot 在 ARM 平台生成的汇编,本质是带重试的 LL/SC 循环。例如对 AtomicInteger 的 CAS,典型序列如下:
立即学习“Java免费学习笔记(深入)”;
- 先用
LDREX加载当前值到寄存器; - 与期望值比较(
CMP); - 若不等,跳转失败路径;
- 若相等,用
STREX尝试写入新值; - 检查
STREX返回码:为 0 表示成功,退出;为 1 表示失败,回到LDREX重试。
注意:即使没有竞争,STREX 也可能失败——比如发生中断、异常、或其它 core 对该 cache line 发起 invalidate 请求(哪怕没写),都会清除 exclusive monitor。这就是所谓 “spurious failure”,也是为什么必须用循环而非一次尝试。
为什么 Java 的 CAS 在 ARM 上需要内存屏障?
LL/SC 本身只保证操作的排他性,不自动约束指令重排序。JVM 在生成 CAS 代码时,会按 Java 内存模型(JMM)要求插入显式屏障:
-
compareAndSet()默认使用memory_order_seq_cst(顺序一致性),对应 ARM 的DMB(Data Memory Barrier); - 读操作前加
LoadLoad和LoadStore屏障,防止上面的读被重排到 CAS 之后; - 写操作后加
StoreStore和LoadStore屏障,确保新值对其他 core 可见前,之前的写已刷新。
这些屏障由 JIT 编译器根据 volatile 语义和原子方法签名自动注入,开发者无需手动写,但理解它们的存在,有助于解释为何看似简单的 CAS 在 ARM 上开销比 x86 略高(尤其在高争用场景下)。
常见陷阱与实际影响
LL/SC 的设计特性带来几个关键现实约束:
- 非嵌套:每个 core 只有一个 exclusive monitor,连续多个 LDREX 会覆盖前一个地址,导致前一个 STREX 必然失败;
- cache line 敏感:若多个原子变量落在同一 cache line(false sharing),一次 STREX 会触发整行失效,大幅降低吞吐;
-
ABA 问题依然存在:LL/SC 只检测地址值是否被改过,不记录修改次数或版本号,因此无法天然规避 ABA;Java 中需配合
AtomicStampedReference或AtomicMarkableReference解决; -
弱 CAS 更常用:Java 的
compareAndSetWeak()或 C++ 的compare_exchange_weak()就是直接映射 STREX 的可能失败行为,避免无谓重试,提升性能。


















