文章目录
一、CAS 原理
1. CAS 概述
- CAS的全称为Compare-And-Swap(或者 Compare-And-Set),通常称为“比较并交换”
- CAS包含3个操作数–内存位置、预期值、更新值
- 它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,否则什么都不做或者重新判断(这种重试行为称为自旋),这个过程是原子的。
2. 硬件级别保证CAS原子性
- CAS是JDK提供的非阻塞原子性操作,它通过硬件保证了比较-更新的原子性
- CAS是一条CPU的原子指令(cmpxchg指令),即是一条CPU并发原语(CAS并发原语体现在JAVA语言中的就是sun.misc.Unsafe类中的各个方法)
- 调用Unsafe类提供的CAS方法(例如:compareAndSwapXXX方法),JVM会帮我们实现出CAS汇编指令cmpxchg。这是一种完全依赖于硬件的功能,通过它实现了原子操作
再次强调,由于CAS是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。
- CAS 的底层是 lock cmpxchg 指令(X86 机器),在单核 CPU 和多核 CPU 下都能够保证【比较-交换】的原子性。
- 在多核状态下,某个核执行到带 lock 的指令时,CPU 会让总线锁住,当这个核把此指令执行完毕,再开启总线。这个过程中不会被线程的调度机制所打断,保证了多个线程对内存操作的准确性,是原子的。这比synchronized重量级锁性能更好。
3. CAS 源码分析
(1)AtomicXXX原子类封装了Unsafe使用的高级API,使用更方便
以AtomicInteger为例:
- AtomicInteger中的compareAndSet方法,调用的是Unsafe.class类中的compareAndSwapInt方法
- AtomicInteger中的getAndIncrement方法,调用的是Unsafe.class类中的getAndAddInt方法
- 变量value用volatile修饰,保证了多线程之间的内存可见性
- AtomicInteger类主要利用CAS+volatile+Unsafe中的native本地方法,保证原子操作,从而避免了加锁,提高了性能
public class AtomicInteger extends Number implements java.io.Serializable {
// 通过反射获取Unsafe对象
private static final Unsafe unsafe = Unsafe.getUnsafe();
// 变量value用volatile修饰,保证了多线程之间的内存可见性
private volatile int value;
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
public final boolean compareAndSet(int expect, int update) {
// 调用的是Unsafe.class类中的compareAndSwapInt方法
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
}
(2)Java中的Unsafe.class类源码
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
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));
return var5;
}
主要对4个参数做说明:
- var1:表示要操作的对象this
- var2:表示变量值在内存中的偏移地址(因为Unsafe内存偏移地址获取数据的)
- var4:表示期望值
- var5/var6:表示更新值
Unsafe类是CAS核心类,由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据
- Unsafe类存在于jdk的sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,Java中CAS操作的执行依赖于Unsafe类的方法。
- Unsafe类提供了非常底层的,操作内存、线程的方法,Unsafe对象不能直接调用,只能通过反射获得(不建议自己反射获得)
注意:Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务
(3)OpenJDK中Unsafe.java源码
假设线程A和线程B同时执行getAndAddInt()方法(分别在不同CPU上执行):
1.假设AtomicInteger里面的value值是3,即主内存中value值是3,根据JMM可知,线程A和线程B会各自持有一份value值为3的副本到工作内存中
2.线程A通过getIntVolatile(o, offset)获取到value为3,此时线程A被挂起
3.线程B通过getIntVolatile(o, offset)获取到value为3,此时线程B没有被挂起并执行compareAndSwapInt方法,比较内存值也为3,则可以更新内存值为4
4.这时线程A恢复,执行compareAndSwapInt方法,比较内存值发现自己的3跟内存的4不一致,说明有其他线程修改过,所以线程A此次更新失败,只能重新再来
5.线程A重新获取value值为4,因为value是volatile修饰的,所以其他线程修改后的值对它可见,线程A重新执行compareAndSwapInt方法,此时可以更新成功
(4)汇编源码
Unsafe类中的compareAndSwapInt,是一个native本地方法,该方法的实现位于unsafe.cpp中。
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x)
UnsafeWrapper("Unsafe_CompareAndSwaplnt");
oop p = JNlHandles::resolve(obj);
// 先想办法拿到变量value在内存中的地址(value传参给p),根据valueOffset偏移量(valueOffset传参给offset),计算value的值
jint* addr = (jint *)index_oop_from_field_offset_long(p, offset);
// 调用Atomic中的函数cmpxchg进行交换比较,x是要交换的值(更新值),e是要比较的值(期望值)
// CAS成功,返回期望值e,等于e,此方法返回true,表示可以进行更新操作
// CAS失败,返回内存中的值,不等于e,此方法返回true,表示不可以进行更新操作
return (jint)(Atomic::cmpxchg(x, addr, e))== e;// JDK提供的CAS机制,在汇编层级会禁止变量两侧的指令优化,然后使用cmpxchg指令比较并更新值
UNSAFE_END
// Atomic::cmpxchg方法
unsigned Atomic::cmpxchg(unsigned int exchange_value, volatile unsigned int* dest, unsigned int compare_value) {
// 根据操作系统类型调用不同平台下的重载函数,在编译期间由编译器决定
return (unsigned int)Atomic::cmpxchg((jint) exchange_value, (volatile jint*) dest, (jint) compare_value);
}
inline jint Atomic::cmpxchg(jint exchange_value, volatile jint* dest, jint compare_value){
// 判断是否为多核CPU
int mp = os::is_MP();
_asm{
// 3个move指令表示将后面的值移动到前面的寄存器上
move edx, dest;
move ecx, exchange_value;
move eax, compare_value;
// CPU原语级别,CPU触发
LOCK_IF_MP(mp);
// cmpxchg:比较并交换指令;
// dword: double word表示2个字(4个字节)
// ptr:pointer指针,与dword连起来使用,表示访问的内存单元是一个双字单元
// 将eax寄存器中的值(compare_value)与[edx]双字内存单元中的值进行比较,若相同,则将ecx寄存器中的值(exchange_value)存入[edx]双字内存单元
cmpxchg dword ptr[edx], ecx
}
}
4. CAS 应用
使用不安全方式和安全方式演示取款案例
- 定义账户接口
/**
* 账户接口,实现了该接口的类,拥有获取余额和取款的功能
*/
interface Account {
// 获取余额
Integer getBalance();
// 取款
void withdraw(Integer amount);
/**
* 方法内会启动 1000 个线程,每个线程做 -10 元 的操作
* 如果初始余额为 10000 那么正确的结果应当是 0
*/
static void demo(Account account) {
List<Thread> ts = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
ts.add(new Thread(() -> {
account.withdraw(10);
}));
}
long start = System.nanoTime();
ts.forEach(Thread::start);
ts.forEach(t -> {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}); // 让所有线程执行完
long end = System.nanoTime();
System.out.println("余额:” + account.getBalance() + " 总耗时: " + (end-start)/1000_000 + " ms");
}
}
(1)使用不安全方式获取共享资源
class AccountUnsafe implements Account {
private Integer balance;
public AccountUnsafe(Integer balance) {
this.balance = balance;
}
@Override
public Integer getBalance() {
return this.balance;
}
@Override
public void withdraw(Integer amount) {
this.balance -= amount; // 这一步不是原子操作,所以是不安全的
}
}
public class TestAccount {
public static void main(String[] args) {
Account unsafeAccount = new AccountUnsafe(10000);
Account.demo(unsafeAccount );
}
}
余额:6860 总耗时: 524 ms
(2)使用安全方式获取共享资源
class AccountCas implements Account {
private AtomicInteger balance;
public AccountCas(int balance) {
this.balance = new AtomicInteger(balance);
}
@Override
public Integer getBalance() {
return balance.get();
}
@Override
public void withdraw(Integer amount) {
// 以下整个while(true) 实现了原子操作,所以是安全的
while(true) {
// 获取余额的最新值
int prev = balance.get();
// 要修改的余额
int next = prev - amount;
// 真正修改
if(balance.compareAndSet(prev, next)) {
break;
}
}
// 整个while(true)里面的代码等同于这一行代码 => balance.getAndAdd(-1 * amount); 后面分析 AtomicInteger 源码
}
}
public class TestAccount {
public static void main(String[] args) {
Account account = new AccountCas(10000);
Account.demo(account);
}
}
余额:0 总耗时: 250 ms
4.1 CAS应用分析
public void withdraw(Integer amount) {
// 需要不断尝试,直到成功为止
while (true) {
// 比如拿到了旧值 100
int prev = balance.get();
// 在这个基础上 100 - 10 = 90
int next = prev - amount;
/*
compareAndSet 正是做这个检查,在 set 前,先比较 prev 与 当前值:
1.如果不一致,next 作废,返回 false 表示失败
比如,别的线程已经做了减法,当前值已经被减成了 90(pre=100 与 当前值90 不一致)
那么本线程的这次 next=90 就作废了,进入 while 下次循环重试
2. 如果一致,以 next 设置为新值,返回 true 表示成功
*/
if (balance.compareAndSet(prev, next)) {
break;
}
}
}
- 线程1 执行 int prev = balance.get(); 从主内存获取到100,再执行int next = prev - amount; 减到90
- 线程2 已经修改到了90,主内存中被设置为90
- 线程1 执行balance.compareAndSet(prev, next),将90设置到主内存之前,先将之前从主内存中获取的100再次和现在主内存中的90比较,发现不一致,则此次next=90不能设置到主内存中,重新获取主内存的值为90,下次在90基础上进行扣减
- 线程1 继续循环执行,同理发现线程2又提前修改到了80,next=80也不能设置到主内存中,下次在80基础上进行扣减
- 最后其他线程都没再修改后,线程1将扣减后的70设置到主内存
6. CAS 与 volatile
volatile(详情见volatile原理)
- 获取共享变量时,为了保证该变量的可见性,需要使用 volatile 修饰。
- 它可以用来修饰成员变量和静态成员变量,可以避免线程从自己的工作内存中获取值,必须到主内存中获取
,线程操作 volatile 变量都是直接操作主内存。即一个线程对 volatile 变量的修改,对其他线程可见。 - volatile 仅仅保证了共享变量的可见性,让其它线程能够看到最新值,但不能解决指令交错问题(不能保证原
子性)
CAS
- CAS 可以保证原子性
所以 CAS 必须借助 volatile 才能读取到共享变量的最新值来实现【比较并交换】的效果。
7. CAS 特点
- CAS 和 volatile 可以实现无锁并发,适用于线程数少、多核 CPU 的场景下(线程数不要超过CPU核心数)。
- CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,可以再
重试。 - synchronized 是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,先加锁,执行完再解锁,其他线程才有机会。
- CAS 体现的是无锁并发、无阻塞并发,请仔细体会这两句话的意思:
- 因为没有使用 synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一,但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响
- 无锁情况下,因为线程要保持运行,需要额外 CPU 的支持,虽然不会进入阻塞,但由于没有分到时间片,仍然会导致上下文切换 ,所以不适用于CPU核数少的场景
8. CAS 两大缺点
(1)自旋循环CPU开销大
- 如果一直得不到锁,就会一直do while循环,消耗CPU
(2) 引发“ABA”问题
- 主线程仅能判断出共享变量的值与最初值 A 是否相同,但不能感知到中间被其他线程从 A 改为 B 又 改回 A 的情况,这就是“ABA”问题