CAS(Compare-And-Swap),它是一条CPU并发原语,用于判断内存中某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。

CAS基本原理

CAS并发原语体现在Java中就是sun.misc.Unsafe类中的各个方法。调用UnSafe类中的CAS方法,JVM会帮我们实现CAS汇编指令。这是一套完全依赖于硬件的功能,通过它实现了原子操作。由于CAS是一种系统原语,原语属于操作系统用语,原语由若干指令组成,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被终端,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致的问题

JDK1.5之后,Java类库才开始使用CAS才开始使用CAS操作,,HotSpot虚拟机在内部对这些方法做了特殊的处理,即时编译出来的结果就是一条平台相关的处理器CAS指令,或者可以认为是无条件内联进去了,不过Unsafe类本来就不是给用户程序调用的类(Unsafe::getUnsafe()的代码里面限制了只有启动类加载器加载的Class才能访问它)

unsafe类分析

如下是Unsafe类中的compareAndSwapInt,是一个本地方法,在unsafe.cpp中
在这里插入图片描述

CAS具体应用
V;内存中地址存放的实际值

O:预期值(旧值)

N:新值,表示我们准备更新V的值

当执行CAS后,如果V == O ,即旧值与内存中实际值相等,表示上次修改后没有任何线程再次修改此值,因此可以将N替换到内存中。

如果 V != O,表示该内存中的值已经被其他线程做了修改,所以无法将N替换,返回最新的值V

当多个线程使用CAS操作一个变量时,只有一个线程会成功,并成功更新变量值,其他线程均会失败。失败线程会重新尝试或将线程挂起(阻塞)

CAS实现原子操作三大问题

CAS的ABA问题

CAS算法实现的一个重要前提需要取出内存中某时刻的数据并在当下时刻比较并替换,那么在这个时间差类会导致数据的变化,比如线程M从内存位置W取出值A,这时N也取出A,N操作后A变成了B,然后N将B又变回A,这时线程M在CAS操作时发现内存中仍然是A,然后M执行成功,M虽然执行成功,但实际上就出现了ABA问题。

解决方案:Java1.5开始,JDK的Atomic包里提供了一个类AtomicStampedRefernce来解决ABA问题。这个类的compareAndSet方法的作用是首先检查当前引用是否等于预期引用,并且检查标志stamped是否为预期标志,如果全部一致,则继续。

CAS的循环时间长开销大问题

自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销,如果JVM能支持处理器提供的pause指令,那么效率会有一定的提升

pause指令的两个作用:

第一,它可以延迟流水线执行指令,使CPU不会消耗过多的执行资源

第二,它可以避免在退出循环的时候因内存顺序冲突而引起CPU流水线被清空

CAS的只能保证一个共享变量的原子操作问题

当对一个共享变量进行原子操作,循环CAS可以解决,但是如果是多个共享变量呢?循环CAS无法解决这个问题。

取巧的话就是合并共享变量:i=2,j=a -> ij=2a; 如此合并就可以操作了

AtomicReference类确实可以保证单个引用对象的原子性,这意味着你可以使用AtomicReference类的compareAndSet方法来安全地比较和设置该引用。然而,它并不能直接保证多个非原子变量之间的原子性操作。

如果你想对多个非原子变量进行原子性操作,可以考虑使用AtomicReference类的一个变种,如AtomicReferenceArray或AtomicStampedReference。这些类提供了更高级别的原子操作,可以同时处理多个变量。

AtomicReferenceArray是一个数组的原子引用,它提供了compareAndSet方法来比较和设置数组中的元素。你可以将多个变量存储在一个数组中,并使用AtomicReferenceArray来保证对这些变量的原子性操作。

例如,如果你有一个包含多个整数的数组,你可以使用AtomicReferenceArray来安全地对其进行原子性操作。以下是一个示例:

import java.util.concurrent.atomic.AtomicReferenceArray;  
  
public class AtomicReferenceArrayExample {  
    public static void main(String[] args) {  
        AtomicReferenceArray<Integer> atomicArray = new AtomicReferenceArray<>(3);  
        atomicArray.set(0, 1);  
        atomicArray.set(1, 2);  
        atomicArray.set(2, 3);  
  
        // 使用 compareAndSet 方法对数组中的元素进行原子性操作  
        boolean success = atomicArray.compareAndSet(1, 2, 4);  
        if (success) {  
            System.out.println("成功将数组中的元素从 2 更改为 4");  
        } else {  
            System.out.println("变量没有改变,仍然为 2");  
        }  
    }  
}

类似地,AtomicStampedReference类提供了一个带有版本号的原子引用,它允许你在单个操作中同时读取和更新引用和一个版本号。这对于需要实现更复杂的原子性条件更新的场景非常有用。

总之,要保证多个变量之间的原子性操作,你需要选择适当的原子引用类来适应你的需求。对于简单的引用比较和设置操作,可以使用AtomicReference。对于多个变量的原子性操作,可以考虑使用AtomicReferenceArray或AtomicStampedReference等更高级别的原子引用类。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐