CAS是个啥

作者: 张依一 | 来源:发表于2019-06-10 23:40 被阅读0次

    CAS (Compare And Swap) 比较并交换

    CAS 是一条CPU并发原语。功能是判断内存某个位置的值是否为预期值,如果是则更改为最新值,这个过程是原子的。CAS并发原语体现在Java语言中就是sun.misc.Unsafe类中的各个本地方法。这是一种完全依赖于硬件的功能,通过它实现了原子操作。原语的执行时连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成数据不一致。

    CAS的作用是比较当前工作内存中的值和主内存中的值,如果相同则执行规定操作,否则继续比较直到主内存和工作内存中的值一致为止。 如果单看这句话不知道在说什么,那么就继续往下看,通篇结束之后再看读一下这句话吧。

    这一篇通过AtomicInteger中两个方法来探索CAS到底是如何实现的。

    AtomicInteger的两个方法

    compareAndSet

    先来看三行代码

    AtomicInteger atomicInteger = new AtomicInteger(5);
    System.out.println(atomicInteger.compareAndSet(5,10)+ "\t current data : " +atomicInteger.get());
    System.out.println(atomicInteger.compareAndSet(5,9)+ "\t current data : " +atomicInteger.get()); 
    

    上篇文章对volatile的理解 提到了,可以使用原子类AtomicInteger 解决原子性问题。这里用到了AtomicInteger.compareAndSet方法,两个参数分别是期望值更新值,如果期望值与此时内存中的值相同,则将内存中的值更新为更新值,方法返回值为布尔类型,表示是否更新成功。以上三行代码,初始化时设置内存中值为5,第二行期望值与更新值分别为5和10,如果期望值5与内存中的值5相同,则将内存中的值更新为10,第二行返回true,当前内存中的值为10;第三行期望值为5,当前内存中的值为10,不符合期望结果,则不更新内存中的结果,返回false,当前内存中的值未更改,还是10。代码输出结果如下:

    true     current data : 10
    false    current data : 10
    

    概述一下,CAS涉及到3个操作数,内存值V,旧的预期值A,要修改的更新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。

    getAndIncrement

    我们知道在多线程情况下i++操作其实包含了三个步骤,是线程不安全的。我们可以使用atomicInteger.getAndIncrement() 实现线程安全的i++,下面通过看下该方法的实现了解如何做到线程安全。

        /**
         * Atomically increments by one the current value.
         *
         * @return the previous value
         */
        public final int getAndIncrement() {
            return unsafe.getAndAddInt(this, valueOffset, 1);
        }
    

    以上是getAndIncrement的源代码,注释在当前值的基础上原子增加一。实现为调用unsafe类的getAndAddInt方法,该方法有三个参数,分别是当前对象、内存偏移量、1,再往下看,就进入到unsafe类。

    1. unsafe类
      unsafe类是CAS的核心类。java是无法直接访问底层系统的,需要通过本地(native)方法来访问,Unsafe的所有方法都是native来修饰的,也就是说Unsafe类中的基础方法都直接调用操作系统底层资源执行相应任务,即可以像C的指针一样直接访问内存。
    2. 变量valueOffset,表示该变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的。以下代码给出了valueOffset的来源
    private static final long valueOffset;
    
        static {
            try {
                valueOffset = unsafe.objectFieldOffset
                    (AtomicInteger.class.getDeclaredField("value"));
            } catch (Exception ex) { throw new Error(ex); }
        }
    
        private volatile int value;
    
    1. 变量value用volatile修饰,保证了多线程之间内存可见性。因此拿到的内存地址总是最新的。

    了解这几个概念,我们继续往下看,getAndAddInt是如何实现的:

        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;
        }
    

    这仍然是存在于unfase的方法,var1对应this,即当前对象,var2为内存偏移量,var4即是要添加的“1”。
    当前方法为do-while结构,根据方法名中getAndAdd,首先get操作,do结构首先根据对象与内存地址获取当前值var5,从物理主内存中拷贝到自己的线程工作内存;然后关键点来了:while里面判断如果这一时刻内存地址中的值与var5相同,则修改为var5+var4,即Add成功,这时返回true,while取反,条件不成立,不再执行do;如果刚拿到var5的值,主物理内存中的值就发生变化了,即主内存与var5的值不一致,则不修改,返回false,while条件满足,继续执行do操作,再次获取当前内存中最新的值,继续判断,直至保证加一时拿到的结果时最新的。
    以上操作简单说,就是拿到var5,然后对var5+1,担心加一操作时var5的值已经发生变化,所以加一时要再验证一遍,如果这个内存中的值还是var5那么就放心的加一操作,否则就不停的去获取最新的值。

    下面举个例子来具体解释上一段内容:

    假设线程A和线程B两个线程同时执行getAndAddInt操作:

    1. AtomicInteger里面的value原始值为3,即主内存中AtomicInteger中的value为3,根据JMM模型(参考上篇文章对volatile的理解),线程A和线程B各自持有值为3的value副本分别到各自的工作内存。
    2. 线程A通过getIntVolatile(var1,var2)拿到value值为3,这是A被挂起。
    3. 线程B也通过getIntVolatile(var1,var2)拿到value值为3,此时刚好相线程B没有被挂起并执行compareAndSwapInt方法比较内存值也是3,成功修改内存值为4,线程B完成操作。
    4. 这时线程A恢复,并执行compareAndSwapInt方法,发现自己工作内存中的数字3与主内存中的数字4不一致,说明该值已经被其他线程抢先修改过了,那么A线程本次修改失败,重新读取最新值重来一遍,即重新执行do操作。
    5. 线程A重新获取value值,因为变量value被volatile修饰,所以其他线程对他的修改,线程A总能第一时间看到,线程A执行compareAndSwapInt进行比较替换,直至成功。

    再概述一下,CAS有3个操作数,内存值V,旧的预期值A,要修改的更新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
    这时再去看开篇的那句话是否就理解了呢。

    CAS的缺点

    1. 循环时间长开销很大
      • 根据源码可以看到,内部采用do-while结构,如果while条件不满足则一直尝试获取新的值,由于CAS没有锁,可能有多个线程同时来修改内存中的值,当多个线程同时在do代码块中不断循环时,给CPU带来很大的开销。而synchronized则保证同一时间只有一个线程在真正修改内存中的值。
    2. 只能保证一个共享变量的原子操作
      • 由于getAndAddInt方法中的第一个参数是object,即this 表示当前对象,从数量上来说这里只能控制一个共享变量。而synchronized加锁能锁住一段代码,可以保证多个线程都被锁住。
    3. 引出来ABA问题
      • ABA问题:线程A要将5修改为10,现在get到内存中的值为5,这是被挂起;线程B将内存中的值从5改为6,然后又从6改为5,B的一顿操作过后A获取资源继续进入while判断,发现内存中的值还是5跟自己手里持有的值相同,可以进行更新操作将5更为10。在A看来get和set操作中间没有发生变化,get的是5,set的时候还是5,实际上里面的值已经发生过变化了。尽管线程A的CAS操作成功,但是不代表这个过程就没有问题。

    相关文章

      网友评论

        本文标题:CAS是个啥

        本文链接:https://www.haomeiwen.com/subject/pquxfctx.html