在Java并发编程中,CAS(Compare-And-Swap)是实现无锁化的“核心黑科技”。它让线程在不加锁的情况下,也能安全地更新共享变量,性能比传统锁高10倍以上。本文从CPU指令→JVM实现→Java应用层层拆解,带你彻底搞懂CAS如何实现无锁编程!
一、CAS的核心思想:乐观锁的“三步赌约”
CAS的全称是比较并交换,它的核心逻辑像一场“赌约”: “假设变量V的值还是我上次看到的A,那么我就把它改成B,否则认输重试。”
🌰 生活类比:
你和朋友共享一个笔记本,你想把“苹果”改成“香蕉”:
先记住当前值是“苹果”(旧值A);准备改成“香蕉”(新值B);最后检查:笔记本还是“苹果”吗?
是 → 改成“香蕉”(成功);否 → 说明被朋友改过,重新看最新值再试(失败,重试)。
💻 CAS的原子操作(伪代码):
boolean cas(V, A, B) {
if (V == A) { // 比较旧值
V = B; // 交换新值
return true;
}
return false;
}
关键点:这三步操作在CPU层面是原子指令,不会被线程调度打断,保证了无锁的原子性。
二、CAS的底层实现:CPU如何保证原子性?
CAS的原子性依赖于CPU的硬件指令,不同架构有不同实现:
1. x86架构:cmpxchg指令
; eax=新值B,ecx=旧值A,edx=变量V的地址
cmpxchg [edx], eax ; 比较V和A,相等则V=B,否则eax=V
原子性保障:CPU通过总线锁或缓存锁,确保指令执行期间其他CPU无法修改V的缓存行。返回值:成功时设置ZF标志位(0),失败时返回内存中的最新值。
2. Java层面:Unsafe类的compareAndSwapInt
Java通过sun.misc.Unsafe类封装了CAS操作,以AtomicInteger为例:
public class AtomicInteger {
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset; // 变量在内存中的偏移地址
static {
try {
valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
}
// Unsafe类的CAS实现(简化版)
public final int getAndAddInt(Object o, long offset, int delta) {
int expect;
do {
expect = unsafe.getIntVolatile(o, offset); // 1. 读旧值
} while (!unsafe.compareAndSwapInt(o, offset, expect, expect + delta)); // 2. CAS尝试
return expect;
}
核心流程:
通过objectFieldOffset获取变量在内存中的偏移地址(精准定位到变量的内存位置);用do-while循环不断重试,直到CAS成功(类似“乐观重试”)。
三、无锁编程的本质:用循环替代锁的阻塞
传统锁(如synchronized)是悲观策略:假设冲突一定会发生,先加锁再操作。 CAS是乐观策略:假设冲突很少,先操作,失败了再重试。
🔄 CAS的无锁循环(以i++为例):
AtomicInteger count = new AtomicInteger(0);
// 线程A和B同时执行i++
public void increment() {
int oldValue, newValue;
do {
oldValue = count.get(); // 读旧值
newValue = oldValue + 1; // 计算新值
} while (!count.compareAndSet(oldValue, newValue)); // CAS失败则重试
}
无锁关键:
线程冲突时,不会阻塞,而是通过循环重试(用户态就能解决,无需陷入内核态);只有在多次重试失败后(比如长时间冲突),才会退化为锁(如AtomicReference的lock操作)。
四、CAS的三大应用场景
1. 原子类(如AtomicInteger)
AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet(); // 内部用CAS实现原子自增
优势:比synchronized快5-10倍(无锁的用户态循环 vs 锁的内核态上下文切换)。
2. 无锁数据结构(如ConcurrentHashMap)
Java 8的ConcurrentHashMap在更新节点时,用CAS替代锁:
// 链表头节点的CAS更新(简化版)
Node
while (!CASHead(tab, i, null, newHead)) {
// 重试直到CAS成功
}
效果:仅锁当前链表的头节点,其他链表仍可并发访问。
3. 原子引用+版本号(解决ABA问题)
AtomicStampedReference
// 旧值100,旧版本1 → 新值200,新版本2
ref.compareAndSet(100, 200, 1, 2);
原理:每次修改同时更新版本号,避免CAS误判A→B→A的中间变化。
五、CAS的“双刃剑”:优点与局限性
✅ 优点:
无锁高性能:避免线程阻塞和上下文切换(适合读多写少、低冲突场景);细粒度控制:仅更新单个变量,不影响其他数据(对比锁的粗粒度);硬件级支持:依赖CPU原子指令,比软件锁(如synchronized)更可靠。
⚠️ 局限性:
ABA问题:值没变但内容可能被修改(用AtomicStampedReference解决);循环开销:高冲突时,大量重试浪费CPU(如100个线程同时修改一个变量);只能保证单个变量原子性:无法原子更新多个变量(需配合AtomicReference封装)。
六、CAS vs 锁:到底该怎么选?
场景推荐方案原因低冲突、读多写少CAS无锁循环快,避免阻塞高冲突、写多读少锁(如synchronized)CAS重试开销大,锁的阻塞更高效复杂数据结构(如链表)CAS+volatile锁粒度可细化到节点(如ConcurrentHashMap的链表头节点CAS)必须保证操作原子性CAS锁可能因异常未释放,CAS依赖硬件原子性更可靠
七、深入理解:CAS为什么能实现无锁?
1. 原子指令的不可分割性
CPU的cmpxchg指令在一个时钟周期内完成“读-比较-写”,期间其他CPU无法修改目标内存(通过缓存一致性协议MESI保证)。
2. 用户态重试避免内核切换
线程冲突时,CAS通过用户态的do-while循环重试,而锁需要陷入内核态挂起线程(一次上下文切换约5000-10000个CPU周期)。
3. volatile的可见性保障
CAS配合volatile变量(如AtomicInteger的value),保证修改立即对其他线程可见(通过内存屏障禁止指令重排序)。
八、总结:CAS的无锁哲学
CAS的核心是“乐观并发控制”:相信冲突很少,通过硬件原子指令和用户态重试,在无锁的情况下实现线程安全。它是Java并发包(java.util.concurrent)的基石,支撑了原子类、并发集合等高性能组件。
记住:
CAS适合低冲突、单个变量、读多写少的场景;高冲突时,CAS的循环重试反而比锁更慢;遇到ABA问题,记得用带版本号的原子类(AtomicStampedReference)。
理解CAS的底层原理,就能在多线程编程中灵活选择“锁”与“无锁”,写出高性能的并发代码~ 💥
觉得有帮助的话,点赞收藏不迷路!下期聊聊“Java内存模型JMM:从可见性到有序性,彻底搞懂多线程数据不一致问题”~