一、引言:并发编程的"原子性"难题
在多线程编程中,原子性 是最基本也最棘手的问题之一。经典的 i++ 操作看似一行代码,在底层却被拆分为"读取-修改-写入"三个步骤。多个线程同时执行时,会出现数据不一致的问题。
传统的解决方案是加锁 (synchronized 或 ReentrantLock),但锁会带来线程上下文切换、阻塞唤醒的开销。在低竞争场景下,加锁的性能损失远大于操作本身。
有没有一种机制,既保证原子性,又避免锁的开销?
CAS(Compare-And-Swap,比较并交换) 给出了答案。它是一种乐观锁技术,允许线程在不加锁的情况下安全地更新共享变量,是现代并发编程的基石。
CAS 的核心思想 :先读取 变量的当前值,然后比较 当前值是否与预期值一致,如果一致则交换 为新值,否则失败重试。整个过程是硬件级别的 原子操作。
二、什么是 CAS?
2.1 定义与操作原语
CAS 是一个原子操作,包含三个操作数:
-
V:要操作的内存位置(变量)
-
E:期望的旧值(Expected value)
-
N:要写入的新值(New value)
执行逻辑:读取 V 的当前值,与 E 比较,如果相等则将 V 更新为 N,否则什么都不做。无论成功与否,都返回 V 的旧值。
java
// CAS 的伪代码表示(实际由硬件原子完成)
boolean compareAndSwap(V, E, N) {
if (V == E) {
V = N;
return true;
}
return false;
}
2.2 为什么 CAS 是"无锁"的?
CAS 操作由CPU 硬件指令 直接支持,在单条指令周期内完成,不会被线程调度中断,因此天然线程安全 。使用 CAS 实现的同步机制,线程不会阻塞,也就没有上下文切换的开销,被称为非阻塞同步。
三、Java 中的 CAS 实现:Unsafe 类
在 Java 中,CAS 操作是通过 sun.misc.Unsafe 类提供的本地方法实现的。Unsafe 是 Java 中用于执行底层、不安全的操作的类,它绕过了 Java 的访问控制,直接操作内存。
3.1 Unsafe 的关键方法
java
// Unsafe 类中的 CAS 方法(简化)
public final native boolean compareAndSwapObject(Object obj, long offset, Object expect, Object update);
public final native boolean compareAndSwapInt(Object obj, long offset, int expect, int update);
public final native boolean compareAndSwapLong(Object obj, long offset, long expect, long update);
-
obj:要操作的对象 -
offset:该对象中字段的内存偏移量(通过Unsafe.objectFieldOffset()获得) -
expect:期望值 -
update:新值
3.2 获取 Unsafe 实例
Unsafe 是单例模式,构造方法私有,只能通过 Unsafe.getUnsafe() 获取,但该方法会检查调用类是否由 Bootstrap ClassLoader 加载,普通应用无法直接调用。
java
// 通过反射获取 Unsafe 实例
public static Unsafe getUnsafe() {
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
return (Unsafe) field.get(null);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
注意 :官方不建议开发者直接使用
Unsafe,因为它的操作不安全,可能导致 JVM 崩溃。但它为java.util.concurrent包提供了底层支撑,我们学习源码即可,不鼓励在业务代码中使用。
3.3 使用 Unsafe 实现自定义原子计数器
java
import sun.misc.Unsafe;
import java.lang.reflect.Field;
public class AtomicCounter {
private volatile long value;
private static final Unsafe UNSAFE;
private static final long VALUE_OFFSET;
static {
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
UNSAFE = (Unsafe) field.get(null);
VALUE_OFFSET = UNSAFE.objectFieldOffset(AtomicCounter.class.getDeclaredField("value"));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public AtomicCounter(long initialValue) {
this.value = initialValue;
}
/**
* 原子性加 1,返回旧值
*/
public long getAndIncrement() {
return UNSAFE.getAndAddLong(this, VALUE_OFFSET, 1L);
}
/**
* 自定义 CAS 更新
*/
public boolean compareAndSet(long expect, long update) {
return UNSAFE.compareAndSwapLong(this, VALUE_OFFSET, expect, update);
}
public long get() {
return value;
}
}
四、源码阅读:AtomicLong 的 CAS 实现
AtomicLong 是 Java 原子包中最常用的类之一,它的底层完全依赖 Unsafe 进行 CAS 操作。
4.1 成员变量与初始化
java
public class AtomicLong extends Number implements java.io.Serializable {
private static final Unsafe U = Unsafe.getUnsafe();
private static final long VALUE;
// 实际存储的值,volatile 保证可见性
private volatile long value;
static {
try {
// 获取 value 字段的内存偏移量
VALUE = U.objectFieldOffset(AtomicLong.class.getDeclaredField("value"));
} catch (ReflectiveOperationException e) {
throw new Error(e);
}
}
}
4.2 getAndIncrement() 源码
java
public final long getAndIncrement() {
// 委托给 Unsafe 的 getAndAddLong
return U.getAndAddLong(this, VALUE, 1L);
}
4.3 Unsafe.getAndAddLong() 源码
java
// sun.misc.Unsafe
public final long getAndAddLong(Object obj, long offset, long delta) {
long v;
do {
// 循环读取当前值
v = getLongVolatile(obj, offset);
// CAS 尝试更新,失败则重试
} while (!compareAndSwapLong(obj, offset, v, v + delta));
return v;
}
核心逻辑:
-
getLongVolatile:读取变量当前值,带volatile语义(从主内存读取) -
compareAndSwapLong:通过 JNI 调用 CPU 指令执行 CAS -
自旋:如果 CAS 失败,循环重试直到成功
4.4 JNI 层面的实现(HotSpot 源码)
Unsafe.compareAndSwapLong 最终调用的是 OpenJDK 中的 JVM 函数:
java
// openjdk/hotspot/src/share/vm/prims/unsafe.cpp
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapLong(JNIEnv *env, jobject unsafe,
jobject obj, jlong offset,
jlong e, jlong x)) {
oop p = JNIHandles::resolve(obj);
volatile jlong* addr = (volatile jlong*)index_oop_from_field_offset_long(p, offset);
// 调用 Atomic::cmpxchg 模板函数,最终映射到 CPU 的 CMPXCHG 指令
return Atomic::cmpxchg(addr, e, x) == e;
}
UNSAFE_END
对于 x86 架构,Atomic::cmpxchg 最终会调用 LOCK CMPXCHG 指令(或 LOCK CMPXCHGQ 对于 long 类型),在多核 CPU 中通过 总线锁定 或 缓存锁定 保证原子性。
五、CAS 的三大问题与解决方案
5.1 ABA 问题
问题描述:线程 T1 读取值 A,然后被挂起;线程 T2 将 A 改为 B 又改回 A;T1 恢复后 CAS 发现值还是 A,误以为没有被修改过,执行更新。
解决方案:
-
AtomicStampedReference:携带版本号(stamp),每次修改版本号+1
-
AtomicMarkableReference:携带布尔标记位
java
import java.util.concurrent.atomic.AtomicStampedReference;
public class ABADemo {
public static void main(String[] args) {
AtomicStampedReference<Integer> ref = new AtomicStampedReference<>(100, 0);
// 线程1:期望值为100,版本号为0,尝试改为101(版本号+1)
int stamp = ref.getStamp();
boolean success = ref.compareAndSet(100, 101, stamp, stamp + 1);
System.out.println("CAS成功?" + success + " 新值:" + ref.getReference());
// 即使别的线程将值改回100,版本号已变,CAS会失败
}
}
5.2 自旋开销问题
问题描述:在高并发下,CAS 失败率高,大量线程自旋重试,消耗 CPU。
解决方案:
-
自适应自旋(JVM 内部优化)
-
使用
LongAdder替代AtomicLong(将竞争分散到多个 Cell) -
失败时让出 CPU(
Thread.yield())或短暂休眠
5.3 单变量原子性限制
问题描述 :CAS 只能原子操作一个共享变量。
解决方案:
-
使用 AtomicReference 包装多个变量
-
或者使用
Lock机制(锁可以原子操作多个变量)
六、性能分析
6.1 CAS vs 锁的性能对比
|-----|----------------|-----------------|
| 场景 | CAS(自旋) | 锁(synchronized) |
| 低竞争 | 极快(无上下文切换) | 较慢(锁获取/释放开销) |
| 中竞争 | 较快(少量自旋) | 一般 |
| 高竞争 | 急剧下降(大量自旋) | 较优(阻塞+调度) |
结论 :CAS 适合低/中竞争 场景,高竞争下建议使用锁或 LongAdder 等分散竞争的方案。
6.2 JVM 对 CAS 的优化
-
锁粗化:将多次 CAS 合并为一次
-
锁消除:逃逸分析后确认无竞争时消除 CAS
-
自适应自旋:根据历史成功率调整自旋次数
七、Java 原子包(java.util.concurrent.atomic)全景
java.util.concurrent.atomic 包提供了丰富原子类,底层均基于 CAS:
|----------------|------------------------------------------------------------------------------------|----------------|
| 分类 | 类名 | 说明 |
| 基础类型 | AtomicInteger、AtomicLong、AtomicBoolean | 原子更新基础类型 |
| 数组 | AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray | 原子更新数组元素 |
| 引用 | AtomicReference、AtomicMarkableReference、AtomicStampedReference | 原子更新引用类型 |
| 字段更新器 | AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater | 原子更新对象字段(基于反射) |
| 累加器(JDK 8) | LongAdder、DoubleAdder | 高并发统计计数,分散竞争 |
| 累积器 | LongAccumulator、DoubleAccumulator | 自定义累积操作 |
7.1 AtomicReference:原子更新引用
java
public class AtomicReferenceDemo {
static class User {
String name; int age;
User(String name, int age) { this.name = name; this.age = age; }
}
public static void main(String[] args) {
User oldUser = new User("old", 20);
AtomicReference<User> ref = new AtomicReference<>(oldUser);
User newUser = new User("new", 25);
// CAS 更新引用
ref.compareAndSet(oldUser, newUser);
System.out.println(ref.get().name); // new
}
}
7.2 AtomicIntegerFieldUpdater:轻量级字段更新
java
public class FieldUpdaterDemo {
static class Candidate {
volatile int score; // 必须 volatile
}
private static final AtomicIntegerFieldUpdater<Candidate> SCORE_UPDATER =
AtomicIntegerFieldUpdater.newUpdater(Candidate.class, "score");
public static void main(String[] args) {
Candidate candidate = new Candidate();
SCORE_UPDATER.incrementAndGet(candidate); // 原子+1
System.out.println(candidate.score); // 1
}
}
八、实战:基于 CAS 实现简易限流器
java
package com.example.ratelimiter;
import java.util.concurrent.atomic.AtomicLong;
/**
* 基于 CAS + 固定时间窗口的简易限流器
*
* 设计目标:限制每秒最大请求数(固定时间窗口算法)
*
* 核心原理:
* - 使用 AtomicLong 作为计数器,保证原子性
* - 使用 volatile 记录窗口起始时间,确保多线程可见性
* - 使用双重检查锁(DCL)确保重置逻辑只执行一次
* - 使用 CAS 自旋实现无锁递增
*
* 注意:这是固定时间窗口(每1秒重置),不是真正的滑动窗口。
* 固定窗口存在临界突发问题(如窗口边界处允许双倍流量),
* 适合对流量均匀性要求不高的场景。
*
* @author YourName
*/
public class FixedWindowRateLimiter {
/**
* 每秒允许的最大请求数(限流阈值)
*/
private final long maxRequestsPerSecond;
/**
* 计数器:统计当前窗口内已通过的请求数
* 使用 AtomicLong 保证原子性,避免多线程竞争
*/
private final AtomicLong counter = new AtomicLong(0);
/**
* 当前窗口的起始时间戳(毫秒)
* volatile 保证多线程间的可见性
*/
private volatile long lastResetTime = System.currentTimeMillis();
/**
* 构造限流器
* @param maxRequestsPerSecond 每秒最大请求数
*/
public FixedWindowRateLimiter(long maxRequestsPerSecond) {
this.maxRequestsPerSecond = maxRequestsPerSecond;
}
/**
* 尝试获取令牌(是否允许通过)
*
* 核心逻辑:
* 1. 检查是否进入下一个时间窗口(距离上次重置 ≥ 1000ms)
* 2. 如果是,使用双重检查锁重置计数器
* 3. 使用 CAS 自旋尝试递增计数器
* 4. 如果递增成功且未超过阈值,返回 true(允许通过)
* 5. 否则返回 false(拒绝)
*
* @return true 允许通过,false 被限流拒绝
*/
public boolean tryAcquire() {
long now = System.currentTimeMillis();
// ===== 阶段1:检查是否需要重置窗口(双重检查锁) =====
// 为什么用 DCL?既保证线程安全,又避免每次都要加锁带来的性能损耗
if (now - lastResetTime >= 1000) {
synchronized (this) {
// 二次检查:防止多个线程同时进入同步块后重复重置
if (now - lastResetTime >= 1000) {
counter.set(0); // 重置计数器
lastResetTime = now; // 更新窗口起始时间
}
}
}
// ===== 阶段2:CAS 自旋递增计数器 =====
while (true) {
long current = counter.get();
// 如果当前计数已超过阈值,直接拒绝(不占用计数)
if (current >= maxRequestsPerSecond) {
return false;
}
// CAS 尝试将计数 +1
if (counter.compareAndSet(current, current + 1)) {
return true; // 成功获取令牌
}
// CAS 失败,说明有其他线程抢先修改了计数器,重试
}
}
/**
* 获取当前窗口内的请求计数(用于监控)
*/
public long getCurrentCount() {
return counter.get();
}
/**
* 获取当前窗口剩余时间(毫秒)
*/
public long getRemainingMillis() {
long now = System.currentTimeMillis();
long elapsed = now - lastResetTime;
return elapsed >= 1000 ? 0 : 1000 - elapsed;
}
// ==================== 测试入口 ====================
public static void main(String[] args) throws InterruptedException {
// 创建限流器:每秒最多 5 个请求
FixedWindowRateLimiter limiter = new FixedWindowRateLimiter(5);
System.out.println("=== 第1轮:连续请求10次(预期前5次通过,后5次拒绝) ===");
for (int i = 0; i < 10; i++) {
boolean allowed = limiter.tryAcquire();
System.out.println("请求 " + i + ": " + (allowed ? "✅ 允许" : "❌ 拒绝"));
}
System.out.println("\n=== 等待1秒,窗口重置 ===");
Thread.sleep(1000);
System.out.println("=== 第2轮:重置后再请求(预期允许) ===");
boolean allowed = limiter.tryAcquire();
System.out.println("请求: " + (allowed ? "✅ 允许" : "❌ 拒绝"));
System.out.println("\n当前计数: " + limiter.getCurrentCount() +
",剩余重置时间: " + limiter.getRemainingMillis() + "ms");
}
}
输出:
请求 0: 允许 请求 1: 允许 ... 请求 5: 拒绝 请求 6: 拒绝 1秒后再次请求: 允许
九、注意事项与最佳实践
✅ 推荐做法
-
优先使用
java.util.concurrent.atomic包提供的原子类 ,不要直接使用Unsafe -
在低/中竞争 场景下使用 CAS,高竞争场景考虑
LongAdder或锁 -
合理设置自旋上限,避免无限循环消耗 CPU
-
使用
AtomicStampedReference解决 ABA 问题 -
利用
AtomicReference实现无锁栈、无锁队列
十、总结
CAS 是 Java 并发包(java.util.concurrent)的基石 。从 AtomicInteger 到 ConcurrentHashMap,从 ReentrantLock 的 AQS 到 LongAdder 的分段计数,处处都有 CAS 的身影。
|-------------|--------------------------------------------------------|
| 维度 | 要点 |
| 操作原语 | Compare-And-Swap:比较并交换,硬件级原子指令(CMPXCHG) |
| Java 实现 | Unsafe 类提供本地方法,Atomic* 类封装使用 |
| 核心优势 | 无锁、非阻塞、低开销(无上下文切换) |
| 主要问题 | ABA 问题、自旋开销、单变量限制 |
| 解决方案 | AtomicStampedReference、LongAdder、AtomicReference |
| 适用场景 | 计数器、状态标志、队列/栈实现、轻量级同步 |
一句话总结 :CAS 用 "失败重试" 替代了 "阻塞等待" ,在无锁状态下实现了线程安全的原子更新,是 Java 高并发性能的底层密码。理解 CAS,就等于拿到了理解 JUC 包的金钥匙。