1. CAS机制的核心概念与背景
CAS(Compare And Swap)是Java并发编程中一种无锁的原子操作机制,它通过硬件指令实现了非阻塞的线程安全控制。在多线程环境下,CAS提供了一种比传统锁机制更高效的同步方案。
1.1 为什么需要CAS
在JDK 5之前,Java主要依靠synchronized关键字实现同步,但这种独占锁机制存在明显缺陷:
- 性能瓶颈:加锁/释放锁导致上下文切换,产生调度延时
- 线程阻塞:一个线程持有锁会导致其他需要该锁的线程全部挂起
- 优先级反转:高优先级线程可能被迫等待低优先级线程释放锁
volatile关键字虽然能保证内存可见性和禁止指令重排,但对于复合操作(如i++)仍无法保证原子性。以下代码展示了volatile的局限性:
java复制public class VolatileExample {
public volatile int count = 0;
public void increment() {
count++; // 这实际上是一个"读取-修改-写入"的复合操作
}
}
1.2 CAS的基本原理
CAS操作包含三个核心参数:
- 内存位置(V):要更新的变量内存地址
- 预期原值(A):认为变量当前应该具有的值
- 新值(B):希望更新为的值
CAS的伪代码逻辑如下:
java复制public boolean compareAndSwap(int V, int A, int B) {
if (V == A) { // 比较内存值与预期值
V = B; // 相等则更新
return true;
}
return false;
}
在Java中,CAS通过sun.misc.Unsafe类提供支持,最终会转换为CPU的cmpxchg指令。以AtomicInteger为例:
java复制public class AtomicInteger extends Number {
private static final Unsafe unsafe = Unsafe.getUnsafe();
private volatile int value;
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
}
2. Java中的CAS实现细节
2.1 硬件层面的支持
CAS的原子性依赖于CPU指令。在x86架构中,对应的指令是cmpxchg(Compare and Exchange)。现代处理器通过以下方式保证其原子性:
- 总线锁定:早期处理器通过LOCK信号锁定总线
- 缓存锁定:现代处理器使用MESI协议,只锁定特定缓存行
JVM会根据处理器类型决定是否为cmpxchg指令添加LOCK前缀。在多核环境下会添加LOCK前缀,单核则省略。
2.2 JVM中的CAS实现路径
Java中的CAS调用链如下:
- AtomicInteger.compareAndSet()
- Unsafe.compareAndSwapInt()
- JNI方法Unsafe_CompareAndSwapInt()
- Atomic::cmpxchg()(平台相关实现)
Windows平台下的实现示例:
cpp复制inline jint Atomic::cmpxchg(jint exchange_value, volatile jint* dest,
jint compare_value) {
int mp = os::is_MP(); // 判断是否多处理器
__asm {
mov edx, dest
mov ecx, exchange_value
mov eax, compare_value
LOCK_IF_MP(mp) // 多核时加LOCK前缀
cmpxchg dword ptr [edx], ecx
}
}
2.3 内存语义
CAS同时具有volatile读和写的内存语义:
- 禁止该指令与前后指令的重排序
- 将写缓冲区数据刷新到内存
- 使其他处理器中对应的缓存行失效
这使得CAS不仅能保证原子性,还能保证可见性。
3. CAS的典型应用场景
3.1 原子类实现
java.util.concurrent.atomic包中的类大量使用CAS:
java复制public class AtomicInteger {
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
}
Unsafe.getAndAddInt()的实现模式:
java复制public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset); // volatile读
} while (!compareAndSwapInt(o, offset, v, v + delta));
return v;
}
3.2 AQS(AbstractQueuedSynchronizer)
AQS是Java并发包的基础框架,其状态变更使用CAS:
java复制protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
3.3 并发容器
如ConcurrentHashMap在JDK 8中的实现:
java复制final V putVal(K key, V value, boolean onlyIfAbsent) {
// ...
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
break;
}
// ...
}
4. CAS的三大问题与解决方案
4.1 ABA问题
问题描述:
线程1读取值为A → 线程2将值改为B又改回A → 线程1的CAS操作仍会成功
解决方案:
使用AtomicStampedReference维护版本号:
java复制AtomicStampedReference<Integer> ref = new AtomicStampedReference<>(100, 0);
// 更新时检查值和版本号
boolean success = ref.compareAndSet(100, 101, stamp, stamp + 1);
4.2 循环时间长开销大
问题表现:
在高竞争环境下,CAS可能长时间自旋,消耗CPU资源
优化方案:
- 使用pause指令减少循环时的能耗(x86的pause指令)
- 适度的自旋后转为阻塞(如AQS的实现)
- 使用LongAdder替代AtomicLong(JDK8+)
4.3 只能保证单一变量原子性
局限性:
无法原子性地更新多个变量
解决方案:
- 使用锁保证多个操作的原子性
- 将多个变量封装成对象,使用AtomicReference
- 使用VarHandle(JDK9+)
5. CAS与锁的性能对比
5.1 低竞争场景
CAS优势明显:
- 无上下文切换开销
- 无用户态/内核态切换
- 线程不会阻塞
测试数据(纳秒/操作):
| 操作类型 | 单线程 | 4线程 |
|---|---|---|
| CAS | 12 | 45 |
| 锁 | 18 | 120 |
5.2 高竞争场景
锁可能更优:
- CAS的自旋会浪费CPU周期
- 锁的等待队列更高效
测试数据(纳秒/操作,32线程):
| 操作类型 | 吞吐量 |
|---|---|
| CAS | 320 |
| 锁 | 280 |
5.3 现代JVM的优化
JDK 6后synchronized进行了重大改进:
- 偏向锁
- 轻量级锁
- 适应性自旋
使得在低竞争时性能接近CAS,高竞争时优于纯CAS。
6. 最佳实践与注意事项
6.1 正确使用模式
推荐的使用模式:
java复制public class Counter {
private AtomicInteger count = new AtomicInteger(0);
public void safeIncrement() {
int oldValue;
do {
oldValue = count.get();
} while (!count.compareAndSet(oldValue, oldValue + 1));
}
}
6.2 性能调优技巧
-
减少CAS竞争:
- 使用线程本地变量
- 采用分片计数(如LongAdder)
-
避免过度自旋:
- 设置合理的自旋次数上限
- 考虑使用Thread.yield()
-
内存布局优化:
- 防止伪共享(@Contended注解)
- 合理安排字段顺序
6.3 常见陷阱
- 复合操作误用:
java复制// 错误用法 - 检查后执行不是原子的
if (atomic.get() == expected) {
atomic.set(newValue); // 这里可能有其他线程修改
}
// 正确做法
atomic.compareAndSet(expected, newValue);
-
ABA问题忽视:
在涉及指针引用的场景(如栈、队列)要特别注意ABA问题。 -
自旋过度:
没有退路策略的CAS可能导致活锁:
java复制// 不好的实现
while (!cas(...)) {
// 空循环
}
// 更好的实现
int spins = 0;
while (!cas(...) && spins++ < MAX_SPINS) {
Thread.onSpinWait();
}
if (spins >= MAX_SPINS) {
// 回退策略
}
7. JVM内部的CAS应用
7.1 对象内存分配
JVM使用CAS管理堆内存分配:
-
指针碰撞分配:
cpp复制HeapWord* new_obj = _top; HeapWord* new_top = new_obj + size; if (new_top <= _end) { if (Atomic::cmpxchg(new_top, &_top, new_obj) == new_obj) { return new_obj; } } -
TLAB优化:
每个线程预先分配一小块内存(Thread Local Allocation Buffer),只在TLAB用完需要新分配时才使用CAS。
7.2 锁优化
偏向锁撤销使用CAS:
cpp复制void ObjectSynchronizer::revoke_bias(oop obj) {
markOop mark = obj->mark();
if (mark->has_bias_pattern()) {
markOop new_mark = mark->set_unbiased();
if (Atomic::cmpxchg(new_mark, obj->mark_addr(), mark) == mark) {
return;
}
}
}
7.3 垃圾收集
CMS和G1收集器使用CAS管理标记位图:
cpp复制bool ConcurrentMark::par_mark(oop obj) {
return _nextMarkBitMap->par_set_bit(obj_addr);
}
8. 替代方案与未来演进
8.1 VarHandle(JDK9+)
提供更精细的内存访问控制:
java复制private static final VarHandle COUNT_HANDLE;
static {
try {
COUNT_HANDLE = MethodHandles.lookup()
.findVarHandle(Counter.class, "count", int.class);
} catch (Exception e) {
throw new Error(e);
}
}
public void increment() {
int oldValue;
do {
oldValue = (int) COUNT_HANDLE.get(this);
} while (!COUNT_HANDLE.compareAndSet(this, oldValue, oldValue + 1));
}
8.2 硬件事务内存
新兴的Intel TSX等特性可能改变并发编程范式:
cpp复制// 伪代码示例
if (_xbegin() == _XBEGIN_STARTED) {
// 事务性执行
shared_var++;
_xend();
} else {
// 回退路径
atomic_increment(&shared_var);
}
8.3 向量化CAS
JDK正在探索的Panama项目可能引入批量CAS操作,提升批量更新的效率。
在实际项目中,我经常遇到开发者在简单场景过度使用CAS,而在复杂场景又不敢使用的情况。经过多次性能测试和问题排查,我的经验是:对于计数器等简单状态变更,优先考虑Atomic类;对于复杂业务逻辑,合理使用锁反而更可靠。特别是在分布式系统中,本地CAS解决不了的问题,需要结合版本号、乐观锁等机制来实现。
