原子类与 CAS
一、CAS(Compare And Swap)
知识点说明
CAS 是无锁并发的基础,也叫乐观锁。它的核心思想:先比较,再交换,全部在一个原子操作中完成。
java
// CAS 的伪代码
boolean compareAndSwap(int[] arr, int offset, int expectedValue, int newValue) {
if (arr[offset] == expectedValue) {
arr[offset] = newValue;
return true; // 更新成功
}
return false; // 更新失败(值被其他线程改了)
}
CAS 由 CPU 硬件提供原子性支持(cmpxchg 指令),不需要加锁,所以称为无锁编程。
CAS 的三个操作数
内存偏移地址 V(要修改的变量的地址)
预期值 A(当前线程认为变量当前值是多少)
新值 B(要修改成什么值)
算法:如果 V 的实际值 == A,则把 V 改为 B,否则不做任何事
Java 中的 CAS 实现
底层通过 Unsafe 类调用 JNI 方法:
java
Unsafe 类提供的 CAS 方法:
boolean compareAndSwapObject(Object o, long offset, Object expected, Object x)
boolean compareAndSwapInt(Object o, long offset, int expected, int x)
boolean compareAndSwapLong(Object o, long offset, long expected, long x)
// offset = 变量在对象中的内存偏移地址(通过 Unsafe.objectFieldOffset() 获取)
CAS 的优缺点
优点:
- 没有锁竞争的开销(不需要上下文切换)
- 性能高(特别是竞争不激烈时)
缺点:
- ABA 问题(下文详述)
- 自旋(循环重试)消耗 CPU
- 只能保证单个变量的原子性
二、ABA 问题
什么是 ABA?
线程 1 读到 A
线程 2 把 A → B → A(改回去)
线程 1 再次 CAS 比较 → 值还是 A → CAS 成功(但其实值已经被改过了)
ABA 的危害
java
// ABA 不影响直接赋值场景(反正值又变回来了)
// 但某些场景有危害:
// 场景:链表操作
// 初始:Head → NodeA → NodeB
// 线程 1:准备 pop NodeA(期望 Head.next == NodeA)
// 线程 2:pop NodeA(Head → NodeB)
// 线程 2:push NodeA 回来(Head → NodeA → NodeD)
// 线程 1:CAS 发现 Head.next 还是 NodeA,认为没变,把 Head 设为 NodeB
// 结果:NodeD 丢失了!
解决方案:AtomicStampedReference
使用版本号(或时间戳)来识别 ABA:
java
// AtomicStampedReference:带版本号的原子引用
AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);
int[] stampHolder = new int[1];
String value = ref.get(stampHolder); // value = "A", stampHolder[0] = 0
// 更新时检查引用和版本号
boolean success = ref.compareAndSet(
"A", // 期望引用
"B", // 新引用
0, // 期望版本号
1 // 新版本号
);
// 也可以使用 AtomicMarkableReference(布尔标记版)
AtomicMarkableReference<String> markRef = new AtomicMarkableReference<>("A", false);
CAS 自旋的代价
java
// 高并发下 CAS 可能失败 → 循环重试(自旋)
// 冲突严重时,自旋次数暴增,CPU 飙升
// 示例:AtomicInteger 的 incrementAndGet
// public final int incrementAndGet() {
// for (;;) { // 自旋
// int current = get();
// int next = current + 1;
// if (compareAndSet(current, next)) // CAS 尝试
// return next; // 成功返回
// } // 失败重试
// }
当冲突激烈时,CAS 性能不如锁——因为大量 CPU 时间消耗在自旋上。
三、原子类(Atomic 系列)
种类一览
| 类别 | 类名 | 说明 |
|---|---|---|
| 基本类型 | AtomicInteger | 原子整型 |
| 基本类型 | AtomicLong | 原子长整型 |
| 基本类型 | AtomicBoolean | 原子布尔型 |
| 引用类型 | AtomicReference | 原子对象引用 |
| 引用类型 | AtomicStampedReference | 带版本号的引用(解决 ABA) |
| 引用类型 | AtomicMarkableReference | 带布尔标记的引用 |
| 数组 | AtomicIntegerArray | 原子整型数组元素 |
| 数组 | AtomicLongArray | 原子长整型数组元素 |
| 数组 | AtomicReferenceArray | 原子引用数组元素 |
| 累加器 | LongAdder(JDK 8) | 高性能原子累加器 |
| 累加器 | LongAccumulator | 支持自定义操作的累加器 |
AtomicInteger 示例
java
AtomicInteger count = new AtomicInteger(0);
// 常用的方法
count.get(); // 获取当前值
count.set(10); // 设置值
count.getAndSet(20); // 获取旧值并设置新值
count.incrementAndGet(); // ++i(先加后取)
count.getAndIncrement(); // i++(先取后加)
count.addAndGet(5); // += 5 并返回
count.compareAndSet(expect, update); // CAS 操作
// 实际应用:计数器
public class RequestCounter {
private final AtomicLong totalRequests = new AtomicLong(0);
private final AtomicLong successRequests = new AtomicLong(0);
public void recordRequest(boolean success) {
totalRequests.incrementAndGet();
if (success) {
successRequests.incrementAndGet();
}
}
public double successRate() {
long total = totalRequests.get();
if (total == 0) return 0;
return (double) successRequests.get() / total;
}
}
AtomicReference 示例
java
// 原子更新对象引用
AtomicReference<String> ref = new AtomicReference<>("初始值");
ref.compareAndSet("初始值", "新值");
// 原子更新对象
AtomicReference<BigDecimal> balance = new AtomicReference<>(BigDecimal.ZERO);
public void deposit(BigDecimal amount) {
while (true) {
BigDecimal current = balance.get();
BigDecimal updated = current.add(amount);
if (balance.compareAndSet(current, updated)) {
break;
}
}
}
四、LongAdder(JDK 8 高性能原子类)
为什么要 LongAdder?
AtomicLong 在高并发下 CAS 竞争激烈,大量线程自旋浪费 CPU。
LongAdder 将单一 value 拆分为多个 cell:
- 竞争不激烈时:直接 CAS 更新 base
- 竞争激烈时:分散到
Cell[]数组,每个线程更新自己的 cell sum()时累加base + Cell[]所有值
AtomicLong 模式: LongAdder 模式:
┌──────────────┐ ┌──────────────┐
│ value │ │ base │ ← 低竞争时使用
│ (一个点) │ ├──────────────┤
└──────────────┘ │ Cell[0] │ ← 线程 A 用
▲ │ Cell[1] │ ← 线程 B 用
│CAS 全挤在这里 │ Cell[2] │ ← 线程 C 用
多线程竞争 │ ... │
└──────────────┘
示例代码
java
// LongAdder vs AtomicLong 对比
LongAdder adder = new LongAdder();
AtomicLong atomicLong = new AtomicLong(0);
// 多线程累加
// 高并发下 LongAdder 性能远超 AtomicLong
adder.increment(); // +1
adder.add(10); // +10
long sum = adder.sum(); // 获取当前总和
long sumThenReset = adder.sumThenReset(); // 获取并重置
// 适用:统计类场景(QPS 统计、请求计数、耗时统计)
// 不适用:需要精确全局序数的场景(如 ID 生成器)
// LongAccumulator — 支持自定义累加函数
LongAccumulator accumulator = new LongAccumulator(Long::max, Long.MIN_VALUE);
accumulator.accumulate(100); // max(初始值, 100) = 100
accumulator.accumulate(50); // max(100, 50) = 100
accumulator.accumulate(200); // max(100, 200) = 200
System.out.println(accumulator.get()); // 200
AtomicLong vs LongAdder 选择
| 对比 | AtomicLong | LongAdder |
|---|---|---|
| 原理 | CAS + 自旋 | 分散 cell,空间换时间 |
| 低并发 | ✅ 性能好 | ✅ 性能好 |
| 高并发 | ❌ CAS 自旋飙升 | ✅ Cell 分散竞争 |
| 空间 | 一个变量 | base + Cell[n] 数组 |
| 适用 | 需要精确值(如 ID 生成) | 统计计数类(如 QPS、总量) |
注意事项
- LongAdder 不保证实时一致性——
sum()遍历 cell 时可能被其他线程修改 sum()是累加不是快照——多线程同时 sum() 可能得到不同结果- LongAdder 适合统计计数,不适合需要精确值作为唯一序列的场景
- AtomicInteger/AtomicLong 的 incrementAndGet() 有返回值,LongAdder 没有(增后值需要通过 sum() 获得,但不精确)
- 高并发下优先用 LongAdder 替代 AtomicLong 作为计数器
五、Unsafe 类(了解即可)
知识点说明
Unsafe 类提供直接操作内存的能力,是 JUC 所有 CAS 操作的底层支撑。
java
// Unsafe 无法直接 new,需要通过反射获取
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);
核心方法
java
// 内存操作
long allocateMemory(long bytes); // 分配堆外内存
void freeMemory(long address); // 释放堆外内存
void putLong(long address, long x); // 写入指定地址
// CAS 操作
boolean compareAndSwapInt(Object o, long offset, int expected, int x);
// 偏移量获取
long objectFieldOffset(Field field); // 获取对象字段的偏移量
// 线程操作
void park(boolean isAbsolute, long time);
void unpark(Object thread);
使用注意
- 不是标准 API——不同 JDK 版本可能变化
- 有安全限制——Java 9+ 模块化后限制更严格
- 不建议直接使用——优先使用 Atomic 系列和并发工具
六、CAS 面试常见问答
Q1:CAS 和锁的对比?
| CAS(乐观锁) | 锁(悲观锁) | |
|---|---|---|
| 思想 | 假设没冲突,先改再说 | 假设一定有冲突,先锁上 |
| 阻塞 | 不阻塞(自旋) | 阻塞/唤醒 |
| CPU 消耗 | 冲突时自旋消耗 CPU | 线程切换消耗 |
| 适用 | 冲突少的场景 | 冲突多的场景 |
Q2:CAS 的三大问题?
- ABA 问题 → 版本号解决(AtomicStampedReference)
- 自旋消耗 CPU → 冲突严重时不如锁
- 只能保证一个变量的原子性 → 多个变量需用锁或 AtomicReference 封装
Q3:LongAdder 为什么比 AtomicLong 快?
空间换时间:每个线程更新自己的 Cell,避免多线程同时对一个变量 CAS 竞争。