1. 什么是CAS?从CPU指令到Java实现
CAS(Compare-And-Swap)是并发编程中的核心原子操作,它的本质是一条CPU指令。现代处理器通过这条指令实现多线程环境下的无锁数据更新。在x86架构中对应的是CMPXCHG指令,ARM架构中则是LDREX/STREX指令对。
CAS操作包含三个关键参数:
- 内存位置(V)
- 预期原值(A)
- 新值(B)
当且仅当V的值等于A时,处理器才会将V的值更新为B,否则不执行任何操作。无论是否更新成功,CAS都会返回V的当前值。这个操作在硬件层面保证了原子性,不会被线程调度打断。
在Java中,CAS通过sun.misc.Unsafe类提供底层支持,但开发者更多是通过java.util.concurrent.atomic包下的原子类来使用CAS。例如AtomicInteger的compareAndSet方法:
java复制public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
注意:虽然Unsafe类提供了强大的底层操作能力,但直接使用它可能导致JVM崩溃等严重问题,官方也不推荐应用代码直接调用。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. CAS在Java并发包中的典型应用场景
2.1 原子变量类
java.util.concurrent.atomic包提供了一系列原子变量类,它们都是基于CAS实现的线程安全类:
- 基本类型:
AtomicInteger、AtomicLong、AtomicBoolean - 引用类型:
AtomicReference、AtomicStampedReference - 数组类型:
AtomicIntegerArray、AtomicLongArray - 字段更新器:
AtomicIntegerFieldUpdater、AtomicReferenceFieldUpdater
以计数器实现为例,对比synchronized和CAS两种方式:
java复制// 使用synchronized
class SynchronizedCounter {
private int count = 0;
public synchronized void increment() {
count++;
}
}
// 使用AtomicInteger
class CasCounter {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet();
}
}
在低竞争环境下,CAS版本的性能通常比synchronized高出2-3个数量级,因为避免了线程挂起和上下文切换的开销。
2.2 并发容器实现
Java并发容器大量使用CAS来优化性能:
ConcurrentHashMap:在JDK8中,put操作首先尝试用CAS插入,失败才转为synchronized块CopyOnWriteArrayList:修改操作通过CAS保证只有一个线程能获得锁执行复制LinkedBlockingQueue:使用两把锁和CAS实现高效的入队出队操作
2.3 AQS(AbstractQueuedSynchronizer)
AQS是Java并发包的核心基础框架,ReentrantLock、CountDownLatch等同步器都基于它实现。AQS内部使用一个volatile int变量表示状态,通过CAS来原子更新这个状态:
java复制protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
3. CAS的三大经典问题与解决方案
3.1 ABA问题
ABA问题是指:
- 线程1读取内存位置X的值为A
- 线程2将X的值从A改为B,然后又改回A
- 线程1执行CAS,发现X的值仍然是A,于是操作成功
虽然最终结果可能没问题,但中间状态的变化可能导致逻辑错误。例如在链表中,节点A被移除后又重新插入,CAS无法感知这个变化。
解决方案:
- 使用
AtomicStampedReference:增加版本号标记 - 使用
AtomicMarkableReference:增加布尔标记
java复制AtomicStampedReference<Integer> atomicStampedRef =
new AtomicStampedReference<>(100, 0);
// 更新时同时检查值和版本号
boolean success = atomicStampedRef.compareAndSet(100, 101, stamp, stamp + 1);
3.2 循环时间长开销大
在高并发场景下,CAS操作可能长时间不成功,导致CPU空转。JVM对此做了优化:
- 在x86架构下,JVM会插入
pause指令减少循环时的能耗 - 对于长时间不成功的CAS,Java会通过自适应自旋策略逐渐放弃
实际编码中可以这样优化:
java复制AtomicInteger counter = new AtomicInteger(0);
int oldValue, newValue;
do {
oldValue = counter.get();
newValue = calculateNewValue(oldValue);
// 限制自旋次数
if (attempts++ > MAX_ATTEMPTS) {
throw new IllegalStateException("Failed after " + MAX_ATTEMPTS + " attempts");
}
} while (!counter.compareAndSet(oldValue, newValue));
3.3 只能保证一个变量的原子操作
CAS只能保证单个变量的原子性,对于多个变量的复合操作无能为力。解决方案包括:
- 使用锁机制
- 将多个变量合并为一个对象,用
AtomicReference包装 - 使用
VarHandle(Java9+)
java复制class Point {
volatile int x, y;
void move(int deltaX, int deltaY) {
// 使用VarHandle保证复合操作的原子性
var handle = MethodHandles.lookup().findVarHandle(Point.class, "x", int.class);
handle.getAndAdd(this, deltaX);
handle = MethodHandles.lookup().findVarHandle(Point.class, "y", int.class);
handle.getAndAdd(this, deltaY);
}
}
4. CAS性能优化实战技巧
4.1 伪共享问题与缓存行填充
现代CPU的缓存系统以缓存行(通常64字节)为单位,当多个线程修改同一个缓存行中的不同变量时,会导致性能下降。这就是伪共享(False Sharing)。
解决方案是缓存行填充:
java复制@sun.misc.Contended // Java8引入的注解
class CounterCell {
volatile long value;
// 填充剩余缓存行
long p1, p2, p3, p4, p5, p6;
}
在ConcurrentHashMap的CounterCell中就使用了这种技术。
4.2 分段计数策略
对于高并发的计数器,可以采用分段计数减少竞争:
java复制class StripedCounter {
private final AtomicLong[] counters;
private static final int N_CPU = Runtime.getRuntime().availableProcessors();
public StripedCounter() {
counters = new AtomicLong[2 * N_CPU];
for (int i = 0; i < counters.length; i++)
counters[i] = new AtomicLong();
}
public void increment() {
int index = ThreadLocalRandom.current().nextInt(counters.length);
counters[index].incrementAndGet();
}
public long get() {
long sum = 0;
for (AtomicLong counter : counters)
sum += counter.get();
return sum;
}
}
4.3 延迟初始化模式
单例模式的双重检查锁定可以用CAS优化:
java复制class Singleton {
private static final AtomicReference<Singleton> INSTANCE = new AtomicReference<>();
public static Singleton getInstance() {
Singleton instance = INSTANCE.get();
if (instance == null) {
instance = new Singleton();
if (INSTANCE.compareAndSet(null, instance)) {
return instance;
} else {
return INSTANCE.get();
}
}
return instance;
}
}
5. CAS与内存模型的关系
5.1 happens-before规则
CAS操作建立了强happens-before关系:
- 对一个volatile变量的写happens-before后续对这个变量的读
- 一个线程中的操作happens-before该线程中的后续操作
5.2 内存屏障
CAS操作会在底层插入内存屏障:
- 在x86上,
LOCK CMPXCHG指令本身就是一个完整的内存屏障 - 在弱内存模型架构(如ARM)上,JVM会插入适当的内存屏障指令
5.3 与volatile的关系
volatile变量的读写与CAS有相似的内存语义:
- 写操作:相当于release语义,确保之前的操作对其它线程可见
- 读操作:相当于acquire语义,确保能看到最新值
但volatile不能保证复合操作的原子性,而CAS可以。
6. 现代JVM对CAS的优化
6.1 内联优化
JIT编译器会将频繁执行的CAS操作内联为机器指令。可以通过JVM参数控制:
code复制-XX:+PrintAssembly // 查看生成的汇编代码
-XX:CompileCommand=print,*AtomicInteger.compareAndSet
6.2 偏向锁与CAS
在偏向锁撤销时,JVM会使用CAS操作来安全地修改对象头:
- 首先尝试通过CAS将对象头中的线程ID替换为当前线程
- 如果失败,说明存在竞争,升级为轻量级锁
6.3 逃逸分析与栈分配
对于不会逃逸的局部Atomic变量,JVM可能通过逃逸分析直接在栈上分配,避免堆分配的开销。
7. 实际项目中的CAS使用经验
7.1 性能监控与调优
可以使用JMH进行CAS性能测试:
java复制@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
public class CasBenchmark {
private AtomicInteger counter = new AtomicInteger(0);
@Benchmark
public int increment() {
return counter.incrementAndGet();
}
}
典型结果可能显示:
- 低竞争:每秒数千万次操作
- 高竞争:性能急剧下降,可能不如锁
7.2 调试技巧
使用-XX:+TraceBiasedLocking可以跟踪偏向锁与CAS的交互:
code复制[0.036s][info][biasedlocking] Revoking bias of object 0x000000071f6d9e98
[0.036s][info][biasedlocking] Saved mark 0x0000000000000005
[0.036s][info][biasedlocking] Saved thread 0x00007f8cbc00e800
[0.036s][info][biasedlocking] New owner 0x0000000000000000
7.3 常见陷阱
- 过度乐观:在写多读少的场景,CAS可能导致大量重试
- 活锁风险:多个线程相互让导致都无法前进
- 优先级反转:低优先级线程持有CAS资源,高优先级线程空转
解决方案:
- 对于写多场景,考虑使用
LongAdder代替AtomicLong - 引入随机退避策略
- 合理设置线程优先级
8. Java并发编程的未来演进
8.1 VarHandle API
Java9引入的VarHandle提供了更灵活的原子操作:
java复制class Point {
private int x, y;
private static final VarHandle X;
static {
try {
X = MethodHandles.lookup()
.findVarHandle(Point.class, "x", int.class);
} catch (ReflectiveOperationException e) {
throw new Error(e);
}
}
void incrementX() {
X.getAndAdd(this, 1);
}
}
8.2 外部内存访问API
Java14引入的Foreign-Memory Access API可以与CAS结合:
java复制try (ResourceScope scope = ResourceScope.newConfinedScope()) {
MemorySegment segment = MemorySegment.allocateNative(100, scope);
VarHandle intHandle = MemoryHandles.varHandle(int.class, ByteOrder.nativeOrder());
intHandle.compareAndSet(segment, 0, 0, 42); // CAS操作
}
8.3 虚拟线程与CAS
Java19引入的虚拟线程(协程)与CAS的结合将带来新的编程模式。虚拟线程的轻量级特性使得CAS失败时的代价更低,可能改变一些并发模式的选择策略。
