CAS运行机制详解
1. CAS的出现背景
1.1 没有CAS之前的问题
在多线程环境下,为了保证线程安全的i++操作(基本数据类型),我们必须使用synchronized等重量级锁机制:
java
public class CounterWithLock {
private int count = 0;
public synchronized void increment() {
count++; // 需要加锁保证原子性
}
public synchronized int getCount() {
return count;
}
}
传统加锁方式的问题:
1.2 有了CAS之后的改进
使用原子类(java.util.concurrent.atomic)可以在多线程环境下保证线程安全的i++操作,类似乐观锁的机制:
java
public class CounterWithAtomic {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.getAndIncrement(); // 无需加锁,CAS保证原子性
}
public int getCount() {
return count.get();
}
}
CAS机制的优势:
2. CAS是什么
2.1 基本概念
CAS是Compare And Swap 的缩写,中文翻译成比较并交换,是实现并发算法时常用到的一种技术。
CAS包含三个操作数:
- 内存位置(V):要更新的变量的内存地址
- 预期原值(A):期望的当前值
- 更新值(B):要设置的新值
2.2 CAS操作原理
执行CAS操作的时候,将内存位置的值与预期原值比较:
- 如果相匹配:处理器会自动将该位置值更新为新值
- 如果不匹配:处理器不做任何操作,多个线程同时执行CAS操作只有一个会成功
CAS操作的核心逻辑:
- 读取步骤:从内存位置V读取当前值
- 比较步骤:将读取到的当前值与预期值A进行比较
- 交换步骤:如果相等,则将内存位置V的值更新为新值B;如果不相等,则不做任何操作
- 返回步骤:返回操作是否成功的结果
伪代码表示:
java
// CAS操作的伪代码实现
public boolean compareAndSwap(int* ptr, int expected, int newValue) {
if (*ptr == expected) {
*ptr = newValue;
return true; // 操作成功
}
return false; // 操作失败
}
2.3 CAS的核心特性
CAS有3个操作数:
- V(Value):位置内存值
- A(Assumed):旧的预期值
- B(New):要修改的更新值
当且仅当旧的预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做或重来。当它重来重试的这种行为称为------自旋!
2.4 硬件级别的保证
CAS是JDK提供的非阻塞 原子性操作,它通过硬件保证了比较-更新的原子性:
- 非阻塞性:不会造成线程阻塞,效率更高
- 原子性:通过硬件保证,更可靠
- CPU指令级别:CAS是一条CPU的原子指令(cmpxchg指令)
现代CPU的CAS实现机制:
- 缓存行锁定:现代CPU不会锁定整个总线,而是锁定特定的缓存行(Cache Line)
- 缓存一致性协议:通过MESI等协议确保多核间的数据一致性
- 性能优化:相比锁总线,锁缓存行的粒度更小,性能更好
- 原子性保证:在缓存行级别保证CAS操作的原子性
CAS的原子性实际上是CPU通过缓存行锁定实现的,比起用synchronized重量级锁,这里的排他时间要短很多,锁定粒度也更小,所以在多线程情况下性能会比较好。
3. CAS底层原理
3.1 Unsafe类的作用
Unsafe是CAS的核心类,它提供了直接操作内存的能力:
java
public class AtomicInteger extends Number implements java.io.Serializable {
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
}
Unsafe类的特点:
- 直接内存访问:Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据
- 本地方法调用:Unsafe类中的所有方法都是native修饰的,直接调用操作系统底层资源
- 内存偏移地址:变量valueOffset表示该变量值在内存中的偏移地址
- volatile保证可见性:变量value用volatile修饰,保证了多线程之间的内存可见性
3.2 AtomicInteger的getAndIncrement()实现
java
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2); // 获取当前值
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); // CAS操作
return var5;
}
CAS操作流程详解:
3.3 compareAndSwapInt方法分析
java
// Unsafe类中的native方法
public final native boolean compareAndSwapInt(
Object var1, // 对象实例
long var2, // 内存偏移量
int var4, // 期望值
int var5 // 新值
);
参数说明:
- var1:表示要操作的对象
- var2:表示要操作对象中属性地址的偏移量
- var4:表示需要修改数据的期望值
- var5:表示需要修改为的新值
3.4 底层CPU指令实现
4. CAS的缺点与自旋锁
4.1 自旋锁机制
自旋锁(spinlock)是CAS实现的基础。CAS利用CPU指令保证了操作的原子性,以达到锁的效果。自旋是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁。
自旋锁的优缺点:
- 优点:减少线程上下文切换的消耗
- 缺点:循环会消耗CPU资源
4.2 CAS的主要缺点
4.2.1 循环时间长开销很大
当多个线程同时竞争同一个原子变量时,失败的线程会不断自旋重试,导致CPU使用率飙升:
java
// 高并发场景下的性能问题示例
public class CASPerformanceTest {
private AtomicInteger counter = new AtomicInteger(0);
public void highConcurrencyIncrement() {
// 1000个线程同时执行increment
// 大量线程会在CAS操作上自旋
counter.getAndIncrement();
}
}
4.2.2 ABA问题
ABA问题的产生:
CAS算法实现的一个重要前提是需要取出内存中某时刻的数据并在当下时刻比较并替换,在这个时间差内会导致数据的变化。
但不知道A值已经被修改过
ABA问题示例:
java
public class ABAExample {
private AtomicReference<String> atomicRef = new AtomicReference<>("A");
public void demonstrateABA() {
// 线程1
new Thread(() -> {
String value = atomicRef.get(); // 读取到"A"
System.out.println("线程1读取到: " + value);
// 模拟一些处理时间
try { Thread.sleep(1000); } catch (InterruptedException e) {}
// 尝试CAS操作
boolean success = atomicRef.compareAndSet(value, "C");
System.out.println("线程1 CAS结果: " + success); // 可能成功,但A值已经被改过
}).start();
// 线程2
new Thread(() -> {
// 快速执行A->B->A的变化
atomicRef.compareAndSet("A", "B");
System.out.println("线程2: A->B");
atomicRef.compareAndSet("B", "A");
System.out.println("线程2: B->A");
}).start();
}
}
4.2.3 ABA问题的解决方案
使用AtomicStampedReference,通过版本号(时间戳)来解决ABA问题:
java
public class ABAResolution {
// 使用版本号解决ABA问题
private AtomicStampedReference<String> stampedRef =
new AtomicStampedReference<>("A", 1);
public void solveABA() {
// 线程1
new Thread(() -> {
int[] stampHolder = new int[1];
String value = stampedRef.get(stampHolder); // 获取值和版本号
int stamp = stampHolder[0];
System.out.println("线程1读取到值: " + value + ", 版本号: " + stamp);
try { Thread.sleep(1000); } catch (InterruptedException e) {}
// 使用版本号进行CAS操作
boolean success = stampedRef.compareAndSet(value, "C", stamp, stamp + 1);
System.out.println("线程1 CAS结果: " + success);
}).start();
// 线程2
new Thread(() -> {
int[] stampHolder = new int[1];
String value = stampedRef.get(stampHolder);
int stamp = stampHolder[0];
// A->B
stampedRef.compareAndSet(value, "B", stamp, stamp + 1);
// 重新获取当前状态
value = stampedRef.get(stampHolder);
stamp = stampHolder[0];
// B->A
stampedRef.compareAndSet(value, "A", stamp, stamp + 1);
System.out.println("线程2完成A->B->A操作,最终版本号: " + (stamp + 1));
}).start();
}
}
版本号机制的工作原理:
AtomicStampedReference源码深度分析
AtomicStampedReference 的设计确实每次更新都创建新对象,但这正是它解决 ABA 问题的关键。下面从使用到底层源码逐步分析:
一、使用层面(解决 ABA 问题)
java
AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 1);
// 线程1:尝试修改值
int[] stampHolder = new int[1];
String current = ref.get(stampHolder); // 返回"A",stampHolder[0]=1
// 线程1在此处暂停
// 线程2:修改两次(A->B->A)
ref.compareAndSet("A", "B", 1, 2); // 成功
ref.compareAndSet("B", "A", 2, 3); // 成功(值变回A,但版本号变为3)
// 线程1恢复执行:
boolean success = ref.compareAndSet(
"A", // 预期值仍是A
"C", // 新值
stampHolder[0], // 预期版本号=1
4 // 新版本号
); // 返回 false!因为实际版本号已是3
二、源码解析(为什么对象不同仍能判断)
1. 核心比较逻辑
java
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
Pair<V> current = pair; // 获取当前Pair对象
// 第一步:比较内容而非对象地址!
return
expectedReference == current.reference && // 比较引用值
expectedStamp == current.stamp && // 比较版本号值
// 第二步:检查是否无需更新
((newReference == current.reference &&
newStamp == current.stamp) ||
// 第三步:执行CAS更新
casPair(current, Pair.of(newReference, newStamp)));
}
2. 关键点:对象不同但内容相同
比较对象 | 比较方式 | 说明 |
---|---|---|
Pair对象 | 引用地址比较 | 通过==比较对象地址 |
reference | 值比较 | 通过==比较引用指向的对象 |
stamp | 整数值比较 | 通过==比较基本类型值 |
当线程1执行CAS时:
expectedReference ("A")
与current.reference ("A")
值相同 ✓expectedStamp (1)
与current.stamp (3)
值不同 ✗- → 条件失败!
三、内存模型解析
java
volatile Pair<V> pair; // 关键volatile变量
private boolean casPair(Pair<V> cmp, Pair<V> val) {
return UNSAFE.compareAndSwapObject(
this,
pairOffset,
cmp, // 预期原对象地址
val // 新对象地址
);
}
对象创建流程:
-
初始状态 :
pair = Pair@1001(reference="A", stamp=1)
-
线程2第一次更新:
casPair(Pair@1001, Pair@1002("B",2))
- → 成功:
pair
指向新对象Pair@1002
-
线程2第二次更新:
casPair(Pair@1002, Pair@1003("A",3))
- → 成功:
pair
指向新对象Pair@1003
-
线程1尝试更新:
casPair(Pair@1001, Pair@1004("C",4))
- → 失败:当前
pair
是Pair@1003
≠ 预期的Pair@1001
四、设计精妙之处
1. 内容比较 vs 对象比较
- 虽然每次创建新对象,但比较的是对象内部的值(引用值+版本号),而非对象本身地址
2. 双重验证机制
- 先验证值:确保当前状态符合预期
- 再验证对象地址:确保期间未被修改
3. 内存可见性保证
volatile
保证每次读取的都是最新对象,而线程本地的current
是读取时的快照
4. 不可变对象(Immutable)
Pair
被设计为不可变(final字段),一旦创建永不改变,避免并发修改
java
private static class Pair<T> {
final T reference; // 不可变引用
final int stamp; // 不可变版本号
private Pair(T reference, int stamp) {
this.reference = reference;
this.stamp = stamp;
}
static <T> Pair<T> of(T reference, int stamp) {
return new Pair<T>(reference, stamp);
}
}
5. 完整的CAS操作流程
这种设计确保了即使值相同,只要期间发生过修改(版本号不同),CAS操作就会失败,从而彻底解决了ABA问题。
4.3 CAS适用场景分析
5. CAS实现机制总结
5.1 CAS的核心优势
- 无锁化:避免了传统锁机制的线程阻塞和上下文切换
- 原子性:通过硬件指令保证操作的原子性
- 高性能:在低竞争场景下性能优异
- 乐观策略:基于乐观锁思想,假设冲突较少
5.2 CAS的技术栈层次
5.3 CAS vs 传统锁对比
特性 | CAS | 传统锁(synchronized) |
---|---|---|
阻塞性 | 非阻塞 | 阻塞 |
性能 | 低竞争时高性能 | 有固定开销 |
实现方式 | 硬件原子指令 | 操作系统互斥量 |
线程切换 | 无 | 可能发生 |
适用场景 | 低竞争、简单操作 | 复杂临界区、高竞争 |
ABA问题 | 存在 | 不存在 |
饥饿问题 | 可能存在 | 可通过公平锁解决 |
5.4 最佳实践建议
- 场景选择:在低竞争、简单操作场景下优先考虑CAS
- 性能监控:高并发场景下监控CPU使用率,避免过度自旋
- ABA防护:需要时使用AtomicStampedReference或AtomicMarkableReference
- 回退策略:在CAS性能不佳时考虑回退到传统锁机制
- 合理设计:避免在CAS操作中包含复杂逻辑
CAS作为现代并发编程的重要技术,在正确使用的前提下能够显著提升程序性能,但需要根据具体场景权衡其优缺点,选择最适合的并发控制策略。