一、 概念:什么是 CAS?
CAS(比较并交换)是一种无锁并发的原子操作策略(属于乐观锁)。
它包含三个操作数:
V (Value):内存中的实际当前值。
A (Expected):预期原值(旧值)。
B (New):准备更新的新值。
核心逻辑:当且仅当内存值V等于预期值A时,才将内存值修改为B。否则,说明值已经被其他线程改过了,本次操作失败。整个过程是一个不可分割的原子操作。
二、 底层实现:从 Java 到 CPU 指令
CAS 在 Java 中的实现是一条自顶向下的调用链:
Java 应用层:我们通常使用
AtomicInteger等原子类,或者ReentrantLock的底层。Java API 层:调用
sun.misc.Unsafe类提供的native方法,如compareAndSwapInt。JNI 桥接层(
unsafe.cpp):通过对象引用和内存偏移量(Offset)计算出属性的绝对物理地址。
将值交给底层原子操作方法
Atomic::cmpxchg。
硬件指令层:在 x86 架构下,最终编译为带有
lock前缀的cmpxchg指令。原子性:
lock前缀会锁定缓存行(或总线),保证“读-比较-写”的硬件级别原子性。可见性/有序性:
lock前缀自带全内存屏障(Memory Barrier)效果,强制刷新主存,并禁止指令重排序。
三、 优点与缺点
优点:
无锁并发,性能极高:无需加锁,不涉及操作系统的互斥量(Mutex),避免了用户态与内核态的上下文切换开销。
非阻塞:如果 CAS 失败,线程不会像
synchronized那样被挂起阻塞,而是可以继续尝试(自旋),或者去做别的事情。
缺点:
只能保证一个变量的原子操作:对于多个变量同时修改,CAS 无能为力(可以使用
AtomicReference封装为一个对象,或者干脆加锁)。ABA 问题(详见下文)。
四、 存在的问题及解决方案(核心考点)
CAS 虽然好,但它天生存在三大“隐患”:
1. 自旋开销大 (CPU 开销)
问题:如果 CAS 一直失败(高并发场景),Java 层的
do-while循环(自旋)会一直占用 CPU 时间片空转。如果长时间不成功,会给 CPU 带来极大的开销。解决:
控制自旋次数(如 JVM 的自适应自旋)。
或者直接退化为锁(如
synchronized的锁膨胀)。或者使用
LongAdder分散热点数据(类似分段锁的思想)。
2. ABA 问题(最经典)
问题:假设线程 1 执行 CAS 时,期望内存值为
A。但在此时,线程 2 抢先将内存改成了B,然后又改回了A。此时线程 1 的 CAS 操作依然会成功,但线程 1 并不知道这中间发生过变化(比如链表节点的值被删了又加回来,可能导致引用错乱)。解决:引入版本号(Version)。每次修改不仅修改值,还要修改版本号(类似数据库的乐观锁)。Java 提供了
AtomicStampedReference类,内部用int stamp记录版本;或者使用AtomicMarkableReference(用 boolean 记录是否被修改过)。
3. 只能保证一个共享变量的原子操作
问题:一个 CAS 指令只能针对一个内存地址(一个变量)操作。
解决:
将多个变量封装成一个对象,使用
AtomicReference对对象进行整体 CAS 替换。使用
synchronized或Lock直接加锁。
💡 总结一句话
Java 的 CAS 是通过Unsafe类的本地方法,最终依靠 CPU 的lock cmpxchg指令实现硬件级别的原子性和可见性。它以极低的性能开销取代了传统的悲观锁,但必须防范其高并发下的 CPU 自旋开销以及常见的 ABA 问题。