CAS介绍
CAS(Compare and Swap),意思就是比较并交换 是一种无锁并发编程的核心原子操作
为什么需要CAS
用于在多线程环境下实现线程安全的数据更新,同时避免了传统锁机制带来的性能开销和复杂性
CAS原理
三参数:
内存地址(V):要修改的共享变量
期望值(A):期望线程读取到的原始值
新值(B):需要更新的目标值
public boolean compareAndSwap(V, A, B) {
if (v== A) {
// 比较当前值与期望值
V = B; // 条件成立则更新为新值
return true;
}
return false;
}
CAS操作涉及三个操作数:内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置的值更新为新值。否则,操作失败,处理器不做任何事情
原子性:整个过程由硬件(CPU 指令)保证原子性,不会被线程调度打断
优点
1.无需锁:CAS操作本身就是一个原子操作,因此可以在不使用锁的下实现多线程环境下的同步。
2.减少上下文切换:相比于传统的锁机制,使用CAS可以减少线程上下文切换的次数,提高性能。
3.易于实现:CAS的实现相对简单,可以方便地在多线程环境中使用
缺点
1.循环时间长的问题:在并发量较大的情况下,CAS操作可能因为一直获取不到正确的预期值而陷入无限循环,导致CPU使用率上升。
2.只能保证一个共享变量的原子性:如果需要对多个共享变量进行原子操作,则CAS无法直接实现,需要结合其他手段,如使用AtomicReference或者其他同步机制。
3.ABA问题
CAS的实现方式
1.硬件支持
CPU 指令:x86 架构的 CMPXCHG 指令,ARM 的 LL/SC(Load-Link/Store-Conditional)指令。
2.java中的实现方式
在java中,在Java中,Unsafe类提供了访问底层硬件的能力,包括执行CAS操作,Unsafe类中有几个关键方法用于实现CAS操作
1.compareAndSwapInt:用于整型变量的CAS操作。
2.compareAndSwapLong:用于长整型变量的CAS操作。
3.compareAndSwapObject:用于引用类型的CAS操作。
不过看名字也知道 不安全的类,Unsafe涉及到底层操作,容易出现安全性问题,推荐使用原子类Atomic,如AtomicInteger通过反射等方式间接使用了Unsafe类来实现CAS操作
3.Atomic 原子类的实现方式
Atomic 原子类提供了一些CAS原子操作方法,虽然底层还是依靠Unsafe类
incrementAndGet();
// AtomicInteger类中
public boolean weakCompareAndSet(boolean expect, boolean update) {
int e = expect ? 1 : 0;
int u = update ? 1 : 0;
return unsafe.compareAndSwapInt(this, valueOffset, e, u);
}
updateAndGet()方法
//原子性地将当前值递增1
public final int incrementAndGet() {
//通过反射间接使用了Unsafe类方法
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
//使用应用给定函数的结果原子地更新当前值,并返回更新后的值 如果失败则重试
public final int updateAndGet(IntUnaryOperator updateFunction) {
int prev, next;
do {
prev = get();
next = updateFunction.applyAsInt(prev);
} while (!compareAndSet(prev, next));//这里看出缺点 CAS操作循环重试导致的CPU消耗
return next;
}
方法使用实例
AtomicInteger atomicInt = new AtomicInteger(0);
// 原子递增(类似 i++)
int newValue = atomicInt.incrementAndGet();
// 自定义更新
atomicInt.updateAndGet(value -> value * 2);
Atomic 原子类中常见涉及CAS操作的类
- AtomicInteger、AtomicLong:整型原子操作。
- AtomicReference:对象引用原子操作。
- AtomicStampedReference:带版本号的引用(解决 ABA 问题)
CAS 的典型问题与解决方案
ABA 问题
线程1 读取值A 准备进行操作
在此期间 线程2将值A–>改变成值B–>又改回值A
对于线程1而言,虽然它再次检查时变量的值仍然是A,看上去好像没有发生什么,但实际上这个变量已经被其他线程修改过,这个问题之所以被称为ABA,变量经历了一个从A到B再回到A的过程
解决方案:
1.版本号机制:在变量上附加一个版本号。每次更新变量时,同时增加版本号
使用 AtomicStampedReference 或 AtomicMarkableReference
AtomicStampedReference<Integer> ref = new AtomicStampedReference<>(100, 0);
// 更新时检查值和版本号
ref.compareAndSet(100, 200, 0, 1);
2.使用锁Lock
使用锁可以确保只有一个线程可以修改变量,从而避免ABA问题。常用的锁包括 synchronized和ReentrantLock。虽然这种方法可能会影响性能,但它能有效地防止ABA问题
public class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
CAS操作总结
CAS 是乐观锁的核心实现,适用于低竞争、简单操作的并发场景。
优势:无锁、高性能(无锁减少了锁的消耗)、避免死锁。
劣势:ABA 问题、自旋开销(如比较更新值 如果不对不断循环尝试)、单变量限制。
适合场景:低竞争场景优先使用 CAS,而高竞争或复杂操作结合锁或分段锁。