并发编程(十) - CAS原理

本文深入解析了CAS(Compare-And-Swap)原理,包括其在硬件层面的原子性保障,以及Java中的Unsafe类实现。通过AtomicInteger的源码分析,展示了如何利用CAS实现无锁并发。同时,对比了CAS与volatile的区别,讨论了CAS的特点和潜在问题,如自旋循环的CPU开销和ABA问题。
摘要由CSDN通过智能技术生成

一、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”问题
  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值