1. CAS机制深度解析
在Java并发编程领域,CAS(Compare-And-Swap)是一个绕不开的核心概念。我第一次接触CAS是在处理一个高并发计数器需求时,当时使用synchronized导致性能急剧下降,转而采用AtomicInteger后性能提升了近8倍,这让我深刻认识到CAS的价值。
CAS本质上是一条CPU原子指令,它包含三个操作数:内存位置V、预期原值A和新值B。当且仅当V的值等于A时,处理器才会用B更新V的值,否则不执行更新。整个比较和替换过程作为一个原子操作执行,不会被线程调度机制打断。在x86架构中,对应的汇编指令是cmpxchg,这也是Java中Atomic类族的底层实现基础。
关键点:CAS的核心价值在于它实现了非阻塞算法(Non-blocking Algorithm),相比传统的锁机制,避免了线程上下文切换和调度延迟的开销。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Java中的CAS实现
2.1 Unsafe类的魔法
Java通过sun.misc.Unsafe类提供CAS操作,虽然这个类名听起来很危险(也确实如此),但它却是并发包的基础。我们来看一个典型的CAS调用:
java复制public final native boolean compareAndSwapInt(
Object o, long offset,
int expected, int x);
实际开发中我们更常用的是Atomic系列类。比如AtomicInteger的incrementAndGet()实现:
java复制public final int incrementAndGet() {
return U.getAndAddInt(this, VALUE, 1) + 1;
}
// HotSpot实现
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!weakCompareAndSetInt(o, offset, v, v + delta));
return v;
}
2.2 典型应用场景
- 计数器场景:比如网站的PV统计
java复制AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet();
- 状态标志位:比如服务开关
java复制AtomicBoolean isRunning = new AtomicBoolean(true);
isRunning.compareAndSet(true, false);
- 链式数据结构:ConcurrentLinkedQueue的入队操作
java复制// 简化版实现
Node<E> newNode = new Node<E>(e);
Node<E> oldTail = tail;
for (;;) {
Node<E> realTail = oldTail;
if (realTail.casNext(null, newNode)
&& casTail(oldTail, newNode)) {
break;
}
}
3. CAS的ABA问题与解决方案
3.1 问题重现
假设一个初始值为A的变量:
- 线程1读取A,准备改为B
- 线程2将A改为C,然后又改回A
- 线程1执行CAS,发现当前值仍是A,操作成功
这在某些场景下会导致严重问题,比如栈结构:
code复制初始栈:A -> B -> C
线程1:准备弹出A,读到A.next=B
线程2:弹出A和B,压入D,再压入A
线程1:CAS比较栈顶还是A,操作成功
结果:B被错误地设置为栈顶的next节点
3.2 解决方案
- 版本号机制:AtomicStampedReference
java复制AtomicStampedReference<String> ref =
new AtomicStampedReference<>("A", 0);
// 更新时检查版本
int[] stampHolder = new int[1];
String current = ref.get(stampHolder);
if (!ref.compareAndSet(current, "B",
stampHolder[0], stampHolder[0]+1)) {
// 处理失败
}
- JDK解决方案对比
| 方案 | 原理 | 适用场景 |
|---|---|---|
| AtomicStampedReference | 添加版本戳 | 需要严格版本控制 |
| AtomicMarkableReference | 使用布尔标记 | 简单状态标记 |
| LongAdder | 分散热点 | 高并发统计 |
4. CAS性能优化实践
4.1 伪共享问题
现代CPU的缓存是以缓存行(通常64字节)为单位。当多个原子变量位于同一缓存行时,会导致不必要的缓存一致性流量。通过填充解决:
java复制// 经典填充方案
@sun.misc.Contended
public class PaddedAtomicLong extends AtomicLong {
public volatile long p1, p2, p3, p4, p5, p6 = 7L;
}
JDK8之后可以使用@Contended注解自动处理,需要添加JVM参数:
code复制-XX:-RestrictContended
4.2 自适应自旋策略
JVM会根据历史成功率动态调整自旋次数。我们可以通过参数控制:
code复制-XX:PreBlockSpin=10 // 默认自旋次数
-XX:+UseSpinning // 启用自旋
实测数据对比(i7-11800H, 16线程):
| 场景 | 锁方案(ms) | CAS方案(ms) |
|---|---|---|
| 简单计数器 | 142 | 38 |
| 复杂状态机 | 210 | 75 |
| 链表操作 | 320 | 158 |
5. 常见问题排查
5.1 性能不升反降
现象:使用CAS后吞吐量下降
排查步骤:
- 检查是否存在过度自旋
- 使用JFR查看CAS失败率
- 检查是否出现伪共享
- 考虑改用LongAdder
5.2 死循环问题
典型错误:
java复制// 错误示范
while(!atomicRef.compareAndSet(old, new)) {
// 没有更新old值!
}
正确写法:
java复制Object oldValue = atomicRef.get();
Object newValue = computeNewValue(oldValue);
while(!atomicRef.compareAndSet(oldValue, newValue)) {
oldValue = atomicRef.get(); // 关键:获取新值
newValue = computeNewValue(oldValue);
}
5.3 内存可见性误区
java复制// 错误认为CAS保证所有变量的可见性
AtomicInteger flag = new AtomicInteger(0);
int data = 0;
void thread1() {
data = 42; // 普通写操作
flag.set(1); // 以为能连带保证data可见
}
void thread2() {
if (flag.get() == 1) {
System.out.println(data); // 可能看到0!
}
}
重要提示:CAS只保证目标变量的原子性和可见性,不保证其他变量的可见性,仍需配合volatile或happens-before规则使用。
6. 高级应用模式
6.1 无锁队列实现
基于CAS的Michael-Scott队列实现要点:
java复制class Node<E> {
volatile E item;
volatile Node<E> next;
// CAS方法省略...
}
public class ConcurrentLinkedQueue<E> {
private transient volatile Node<E> head;
private transient volatile Node<E> tail;
public boolean offer(E e) {
Node<E> newNode = new Node<E>(e);
for (Node<E> t = tail, p = t;;) {
Node<E> q = p.next;
if (q == null) {
if (p.casNext(null, newNode)) {
if (p != t) // 每两次更新一次tail
casTail(t, newNode);
return true;
}
}
else if (p == q)
p = (t != (t = tail)) ? t : head;
else
p = (p != t && t != (t = tail)) ? t : q;
}
}
}
6.2 组合CAS操作
当需要同时更新多个变量时,可以采用不可变对象+引用替换:
java复制class Point {
final int x, y; // 不可变
Point(int x, int y) {
this.x = x;
this.y = y;
}
}
AtomicReference<Point> ref = new AtomicReference<>(new Point(0,0));
void move(int dx, int dy) {
Point oldP, newP;
do {
oldP = ref.get();
newP = new Point(oldP.x+dx, oldP.y+dy);
} while (!ref.compareAndSet(oldP, newP));
}
7. JVM层实现揭秘
以HotSpot的x86实现为例,CAS最终会映射到CPU指令:
cpp复制// hotspot/src/os_cpu/linux_x86/vm/atomic_linux_x86.hpp
inline jint Atomic::cmpxchg(jint exchange_value,
volatile jint* dest,
jint compare_value) {
int mp = os::is_MP();
__asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)"
: "=a" (exchange_value)
: "r" (exchange_value), "a" (compare_value),
"r" (dest), "r" (mp)
: "cc", "memory");
return exchange_value;
}
关键点:
- LOCK前缀在多核情况下会锁总线
- cmpxchgl是x86的CAS指令
- 内存屏障保证可见性
8. 最佳实践总结
-
适用场景选择:
- 适合读多写少、竞争不激烈的场景
- 对于写多场景,考虑LongAdder
-
失败处理策略:
java复制// 带重试上限的实现 public boolean tryUpdateWithRetry(AtomicInteger ai, int newValue, int maxRetry) { int oldValue; int retry = 0; do { oldValue = ai.get(); if (retry++ > maxRetry) { return false; } } while (!ai.compareAndSet(oldValue, newValue)); return true; } -
监控指标:
- CAS成功率(可用JMX获取)
- 自旋次数(通过性能分析工具)
- 缓存一致性流量(perf工具)
-
模式选择决策树:
code复制if (操作非常简单且竞争低) → 纯CAS else if (写多读少) → LongAdder else if (需要复合操作) → AtomicReference else if (ABA问题可能) → AtomicStampedReference else → 考虑锁方案
在实际项目中,我通常会先用JMH进行基准测试,对比不同方案的性能表现。曾经在一个交易系统中,通过将synchronized改为CAS+退避策略,使TPS从1200提升到了8500。但也要注意,不是所有场景都适合CAS,当竞争非常激烈时,传统的锁可能反而是更好的选择。
