CAS是什么

发布时间 2023-06-22 21:52:48作者: 哩个啷个波

CAS又称 自旋锁、无锁,是一种乐观锁

img

img

  1. compare and swap 的缩写 意为: 比较并交换 , 实现并发算法的常用技术 , 就是说我不用加锁 , 也能保证 ( 加锁会影响效率,可以考虑使用原子操作类 ) 原子性 , 当多个线程尝试使用 CAS 同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试 , 因此可以知道 , CAS只会允许一个线程的执行成功
  2. CAS 包括三个操作数 - 内存位置 , 预期原值以及更新值
    1. 执行 CAS 操作的时候,将内存位置的值与预期原值比较
      1. 如果相匹配 , 那么处理器会自动的将该位置更新为新值
      2. 如果不匹配 , 处理器不做任何操作 , 多个线程同时执行 CAS 的操作只有一个成功

例如下面的 AtomicInteger 等 原子类 就是基于 CAS 实现的

操作的过程图解:

  1. CAS有三个操作数,位置内存值V , 旧的预期值A , 要修改的更新值B
  2. 当且仅当旧的预期值A 和内存值V相同时 , 将内存值V修改为B , 否则什么都不做或重来

img

img

img

AtomicInteger 执行过程源码:

AtomicInteger atomicInteger=new AtomicInteger();
    public void addAtomic(){
        atomicInteger.getAndIncrement();
    }
 
            // getAndIncrement方法 this是当前AtomicIntegr对象
            // valueOffset是内存偏移量/值
            // 1为自增量
            // Unsafe类的getAndAddInt方法,该Unsafe类是在jdk里的rt.jar包
 public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
 }
 
 
 
  public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            //var1对象里的var2这个内存偏移量的值 
            var5 = this.getIntVolatile(var1, var2);         
            //var1 AtomicInteger 对象本身
            //var2 该对象值得引用地址,内存地址偏移量
            //var4 需要变动的数量 +1
            //var5 是用var1 var2 找出的主内存中真实的值
            //用该 对象当前的值与var5比较
            //如果相同,进行var5+var4并且返回true
            //如果不同,继续从主内存中取值然后 再比较,直到更新完成
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
        return var5;
    }

创建一个原子类对象 ( 默认初始化值为0 ) , 也可以传入定值 , 当我们使用CAS方法时 , 传入一个预期值 , 一个修改值 , 仅当预期值和当前对象 valueOffset的内存偏移量的值相同 , 就可以修改成功 , 前提是没有被其他的线程抢先一步进行比较并交换 , 否则就要继续读取最新的修改值 *( 变量可以使用 volatile关键字解决可见性问题 , 就是说 : 一个线程对共享变量的修改,能够及时的被其他线程看到 , 立刻同步最新数值 )* , 再继续的进行比较并交换这一操作 , 直到修改成功..... 这是比较并交换的源代码 : native为本地方法 , 底层用C 实现对操作系统的访问和操作 (Java不可以直接访问)

*此代码以及执行流程如下 ?

img

img

*while ( ! this.compareAndSwapInt ( var1, var2, var5, var5 + var4 ) ) 的执行流程:*

  • 当两个线程 A , B 同时执行 AtomicInteger 中的自增方法( getAndAddlnt ) 时 , 假如 AtomicInteger 中主内存的值是10
    • 线程 A 调用 getlIntVolatile (var1, var2) 获取一份原始内存值(最新值)10 , 开始要操作了
    • 线程 B 调用 getlIntVolatile (var1, var2) 也获取一份原始内存值(最新值)10 , 开始要操作了
    • 线程 A 抢夺的速度很快, 调用 compareAndSwaplnt 方法 , 预期值和原始值相同都是10 , 比较并修改为11
    • 线程 B 这时调用 compareAndSwaplnt 方法 , 发现自己的期望值是10,但是内存中真正的原始值被修改为11了,被其他线程修改了 , 这时线程A就修改失败 , 重新读取内存中的新值 value (volatile关键字作用: 及时的被其他线程看到最新的 value值 , 共享变量值的修改 )
    • 线程 B 继续进行调用 compareAndSwaplnt 方法 , 比较并修改

*注意 : 这时线程B 还不一定就能修改成功 , 有可能还有其它线程抢夺的速度比它快 , 很快的将期望值 var5 进行了修改 , 线程B比较发现和期望值不匹配 , 因此线程B可能继续下去 , 直到比较成功 , 返回 ture 即可*

为什么原子类可以不加 synchronized 实现原子性 ?


何为原子性:原子就是最小的单位 , 不可分割的 , 就是指一个操作是不可中断的。即使是多个线程一起执行的时候 ,一个线程的操作一旦开始,执行的过程中不允许被中断 , 也就是说不会被其他线程干扰 , 直到执行完毕 , 不会造成数据不一致的问题 , 因此原子类使用 CAS 保证了并发情况下线程的安全性

img

\1. Unsafe 这个类是Java底层的一个类 , 是 CAS 的核心类 , 相当于是一个后门 , 它存在于 sun.misc 包中 , Unsafe 类中的所有的方法都是被 native 关键字修饰的 , 被 native 修饰的方法为本地方法 , 底层是用 C 实现对操作系统的访问和操作 , 就比如调用 UnSafe 类中的 CAS 方法 , 它是通过操作系统的汇编指令来实现操作内存数据的 , 因为Java中 CAS 操作的执行依赖于Unsafe 类的方法

img

\2. Unsafe 通过 对象的 valueOffset 内存偏移地址获取原始内存值 , 用当前工作内存值 (var1,var2) 和期望值 (var5) 进行比较判断是否能替换 , 一旦修改了 , 其他的线程一比较发现工作内存的值和期望值不一样 , 所以就无法操作了 , 只能继续获取新值 (getIntVolatile) , 继续比较 , 这样就保证了一次只能有一个线程在执行并且执行成功 , 就算不加锁 , 其他的线程也是进不来的 , 就无法操作 , 因此我们说 : 原子类可以不加 synchronized 就实现了原子性,保证了线程的安全

img

img

\3. 被 volatile 修饰的变量 , 保证了线程的可见性 , 一旦被修改 , 就能及时的将数据反馈给其他线程 , 以供其他线程获取最新的值

CAS 的缺点


  1. CAS 它是一条 CPU并发原语 , 当执行 getAndAddInt 方法的时候 , 里面有一个do while 方法 , 如果CAS失败 ,会一直进行尝试 比较 , 假如有一百万个线程 , 如果 CAS 长时间一直不成功 , 这么多的线程会直接给CPU打满 , CPU 的开销变大
  2. 当对一个共享变量执行操作时 , 我们可以使用循环 CAS 的方式保证原子操作 , 对多个共享变量操作时,循环CAS就无法保证操作的原子性 , 这个时候就可以用锁来保证原子性。
  3. ABA 问题 :
    1. 线程A从主内存中拿了一份值 , 假如是10 , 它在自己的工作空间中打算进行修改了 (还没修改)
    2. 这时线程B抢占了线程A , 它从主内存中将10取出来了, 它给修改成了22 , 放回主内存中了 , 但是呢 ,它感觉修改的不满意 , 又去主内存把那个22又取出来了 , 又给改成了10 , 然后走了
    3. 这时线程A 去进行比较并交换 , 发现工作内存值和主内存值(期望值)是一样的都是10 , 就成功修改了, 但是 ! 注意 ! 线程A并不知道这个值中途被其他线程修改了 ! ! !

*解决方案:用 AtomicStampedReference 的时间戳版本号解决 ABA 问题 ,进行版本号的比较 , 每次变动时 , 都会产生一个版本号*

img