CAS
在高并发编程中,保证数据一致性一直是个核心难题。传统的互斥锁(如synchronized)通过阻塞线程来避免竞争,但会带来上下文切换的开销。JUC(java.util.concurrent)大量采用了一种无锁算法------CAS(Compare And Swap),它在保证原子性的同时,极大提升了性能。本文将深入理解CAS的原理、应用以及经典的ABA问题。
什么是CAS?
CAS,全称Compare And Swap,即比较并交换。它是一种乐观锁 的实现,依靠硬件指令保证原子性 。它的核心作用是:在不使用锁(如 synchronized)的情况下,保证读取-比较-写入这三步操作不会被其他线程打断。
CAS 操作依托 sun.misc.Unsafe 类下的 compareAndSwapXXX 等方法。代码形态如下(OpenJDK 源码片段):
java
public final native boolean compareAndSwapObject(Object o, long offset,
Object expected, Object x);
public final native boolean compareAndSwapInt(Object o, long offset,
int expected, int x);
public final native boolean compareAndSwapLong(Object o, long offset,
long expected, long x);
这些方法会根据字段在对象中的内存偏移量(offset)找到变量,然后执行CAS操作。由于Unsafe涉及底层内存操作,普通应用代码不能直接获取其实例,但JUC包内的原子类已经帮我们封装好了这些能力。
CAS 核心逻辑⭐
一条完整的 CAS 操作包含三个操作数:
- 内存位置 V :要读取和修改的变量地址(对应代码中的
address) - 预期原值 A :我们认为内存位置中应该存的值(对应代码中的
expectedValue) - 新值 B :希望写入的新值(对应代码中的
swapValue)
java
boolean CAS(address, expectedValue, swapValue) {
if (address 当前的值 == expectedValue) {
address = swapValue; // 将新值写入内存
return true; // 更新成功
}
return false; // 值已被修改,本次不更新
}
参数说明
address:内存地址(变量位置)expectedValue:期望看到的值(CPU 寄存器1的值)swapValue:要设置的新值(CPU 寄存器2的值)
执行逻辑:当且仅当 V 中的值与 A 相等时,CAS才会通过原子方式将 V 的值更新为 B,否则不做任何操作。无论是否更新成功,最终都会返回 V 的旧值。
注意 :实际 CPU 硬件实现中,比较和交换是原子操作,不会被中断。
CAS的应用
a) 原子类(Atomic包)
Java的原子类(AtomicInteger, AtomicLong,AtomicReference 等)正是基于CAS实现的无锁线程安全工具。以 AtomicInteger 的 incrementAndGet 为例:
java
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicCounterDemo {
// 使用原子类 AtomicInteger 代替普通的 int,保证线程安全
// private static int count = 0; // 如果使用此方式,多线程并发累加会导致结果错误
private static AtomicInteger count = new AtomicInteger(0);
public static void main(String[] args) {
// 线程1:对 count 执行 50000 次自增
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
// count.getAndIncrement() 等价于 count++
// 它会原子地返回当前值并加1,底层基于 CAS 实现
count.getAndIncrement();
// 如果是 ++count,可以使用:
// count.incrementAndGet();
// 如果需要增加指定数值 n(如 count += n),可以使用:
// count.addAndGet(n);
}
});
// 线程2:同样对 count 执行 50000 次自增
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
count.getAndIncrement();
}
});
t1.start();
t2.start();
try {
// 主线程等待 t1 和 t2 执行结束
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 期望输出 100000,原子类保证了最终结果的正确性
System.out.println("Count: " + count.get());
}
}

b) 实现自旋锁
利用 AtomicReference 的 CAS 操作,我们可以模拟一把简单的自旋锁。锁的状态由 AtomicReference<Thread> 持有,null 表示释放,非null 表示被某个线程持有。
java
import java.util.concurrent.atomic.AtomicReference;
public class SpinLock {
private AtomicReference<Thread> owner = new AtomicReference<>(null);
public void lock() {
Thread current = Thread.currentThread();
// 自旋直到成功获取锁
while (!owner.compareAndSet(null, current)) {
// 可在此处让出CPU,减少忙等开销,如 Thread.yield();
}
}
public void unlock() {
Thread current = Thread.currentThread();
// 只有持有锁的线程才能释放
owner.compareAndSet(current, null);
}
// 测试代码
public static void main(String[] args) {
SpinLock spinLock = new SpinLock();
Runnable task = () -> {
spinLock.lock();
try {
System.out.println(Thread.currentThread().getName() + " 获得锁");
Thread.sleep(10); // 模拟业务操作
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName() + " 释放锁");
spinLock.unlock();
}
};
new Thread(task, "线程-1").start();
new Thread(task, "线程-2").start();
}
}
CAS成功时,锁被当前线程获取;失败则一直自旋尝试(忙等待)。这种方式避免了线程挂起、唤醒的开销,适合临界区执行时间非常短的场景。
ABA问题
什么是ABA问题?
CAS操作仅比较值是否相等,但无法感知值的变化过程。假设某个变量初始值是A,线程1把它改为B,又线程2(或同线程)把它改回A。此时另一个线程进行CAS检测时,发现值仍然是A,就误认为它没有被修改过,继续执行更新------但事实上变量已经经历过"A→B→A"的变化,这种变化在某些场景下可能引发错误。
解决方案:带版本号的CAS
Java提供了 AtomicStampedReference 和 AtomicMarkableReference 来解决ABA问题。
AtomicStampedReference:内部维护一个[reference, stamp]对,stamp相当于版本号。每次更新时stamp递增,CAS会比较引用和stamp是否同时匹配。AtomicMarkableReference:类似,mark是布尔型的标记,适用于关注节点是否被逻辑删除等场景。
示例:使用 AtomicStampedReference 解决ABA
java
import java.util.concurrent.atomic.AtomicStampedReference;
public class ABAProblemSolution {
public static void main(String[] args) {
String initialRef = "A";
int initialStamp = 0;
AtomicStampedReference<String> stampedRef =
new AtomicStampedReference<>(initialRef, initialStamp);
// 模拟线程1:期望 stamp 为 0,更新为 "C" 并 stamp+1
Thread t1 = new Thread(() -> {
int stamp = stampedRef.getStamp();
String ref = stampedRef.getReference();
// 模拟 ABA 发生
sleep(100);
boolean success = stampedRef.compareAndSet(ref, "C", stamp, stamp + 1);
System.out.println("线程1更新结果: " + success + ",当前引用: " + stampedRef.getReference());
});
// 模拟线程2:执行 A -> B -> A 的ABA操作,同时stamp变化
Thread t2 = new Thread(() -> {
// A -> B (stamp 0->1)
int stamp = stampedRef.getStamp();
stampedRef.compareAndSet("A", "B", stamp, stamp + 1);
System.out.println("线程2: A->B , stamp=" + stampedRef.getStamp());
// B -> A (stamp 1->2)
stamp = stampedRef.getStamp();
stampedRef.compareAndSet("B", "A", stamp, stamp + 1);
System.out.println("线程2: B->A , stamp=" + stampedRef.getStamp());
});
t2.start();
sleep(50); // 保证t2先执行ABA
t1.start();
}
private static void sleep(long ms) {
try { Thread.sleep(ms); } catch (InterruptedException e) { }
}
}
输出示例:
text
线程2: A->B , stamp=1
线程2: B->A , stamp=2
线程1更新结果: false,当前引用: A
因为线程1在尝试CAS时,会检查当前stamp(此时为2)是否与它之前读取的stamp(0)一致,显然不一致,因此CAS失败,避免了ABA问题。
CAS的优缺点
优点
- 高并发性能:无锁设计,线程不会阻塞,避免了上下文切换开销,适合读多写少或临界区极短的场景。
- 天然避免死锁:由于没有锁的持有与等待,不存在死锁风险(但可能存在活锁,即长时间CAS失败重试)。
- 粒度细:可以针对单个变量进行原子操作。
缺点
- ABA问题:需要额外机制处理(如版本号)。
- 自旋开销:在高竞争下,大量线程反复CAS会消耗CPU资源,甚至出现总线风暴。通常配合CPU pause指令或自适应重试策略。
- 只能保证一个共享变量的原子性 :若需要对多个变量进行复合原子操作,仍需要锁机制(或使用
AtomicReference封装一个不可变对象整体更新)。 - 底层实现依赖硬件的原子指令,JVM需要高效地暴露这些能力(如通过Unsafe)。
