美文网首页
JUC之CAS原理

JUC之CAS原理

作者: 西界__ | 来源:发表于2020-12-27 09:18 被阅读0次

    CAS概念

    <mark>CAS的英文为Compare and Swap 翻译为比较并交换。</mark>

    CAS机制中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。

    更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。

    CAS的全称为Compare-And-Swap ,它是一条CPU并发原语.

    它的功能是判断内存某个位置的值是否为预期值,如果是则更新为新的值,这个过程是原子的.

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

    CAS应用

    将CAS的应用前我们要先将一个类,这个类我们已经使用过了,就是下图这个例子。

    public class VolatileDemo {
    
        public static void main(String[] args) {
            MyData myData = new MyData();
            for (int i = 1; i <= 20; i++) {
                new Thread(() -> {
                    for (int j = 1; j <= 1000; j++) {
                        myData.addPlusPlus();
                        myData.addMyAtomic();
                    }
                }, String.valueOf(i)).start();
            }
    
            //需要等待前面20个线程全部执行完,在用main线程取得最终的结果值是多少
            while (Thread.activeCount() > 2) {
                //main 线程 GC线程
                Thread.yield();
            }
            System.out.println(Thread.currentThread().getName() + "\t int type final number value:" + myData.number);
            System.out.println(Thread.currentThread().getName() + "\t AtomicInteger type final number value:" + myData.atomicInteger);
        }
    }
    
    
    class MyData {
        volatile int number = 0;
    
        public void addT060() {
            this.number = 60;
        }
    
        //number加了volatile关键字
        public void addPlusPlus() {
            number++;
        }
    
        AtomicInteger atomicInteger = new AtomicInteger();
    
        public void addMyAtomic() {
            atomicInteger.getAndIncrement();
        }
    }
    

    我们使用原子操作类,指的是java.util.concurrent.atomic包下,一系列以Atomic开头的包装类。如AtomicBoolean,AtomicUInteger,AtomicLong。它们分别用于Boolean,Integer,Long类型的原子性操作。来保证了原子性。

    而Atomic操作类的底层正是用到了“CAS机制”。

    我们在来一个案例,使用原子类的compareAndSet方法

    //CAS compare and swap 比较并交换
    public class CASDemo {
    
        public static void main(String[] args) {
            AtomicInteger atomicInteger = new AtomicInteger(5);
    
            System.out.println(atomicInteger.compareAndSet(5, 2019) + "\t current data: " + atomicInteger.get());
            System.out.println(atomicInteger.compareAndSet(5, 1024) + "\t current data: " + atomicInteger.get());
    
            System.out.println(atomicInteger.getAndIncrement());
        }
    }
    

    CAS原理

    CAS的原理包括Unsafe类和自旋利用unsafe提供的原子性操作方法,首先我们来查看AtomicInteger中的getAndIncrement()源码

    Unsafe类

    return unsafe.getAndAddInt(this, valueOffset, 1);

    • this代表当前对象
    • valueOffset代表内存偏移量
    • 1是常量每次自增1

    unsafe是什么呢?unsafe就是Unsafe类实例对象。

    • 变量ValueOffset,便是该变量在内存中的偏移地址 ,因为UnSafe就是根据内存偏移地址获取数据的
    • 变量value和volatile修饰,保证了多线程之间的可见性.

    可以看到该方法调用的是Unsafe 类的getAndAddInt()方法。我们查看Unsafe类的源码

    我们可以得知Unsafe类存在大量被native修饰的本地方法!

    UnSafe是CAS的核心类 由于Java 方法无法直接访问底层 ,需要通过本地(native)方法来访问,UnSafe相当于一个后面,基于该类可以直接操作特额定的内存数据.UnSafe类在于sun.misc包中, 其内部方法操作可以向C的指针一样直接操作内存,因为Java中CAS操作的助兴依赖于UNSafe类的方法.

    <mark>注意UnSafe类中所有的方法都是native修饰的,也就是说UnSafe类中的方法都是直接调用操作底层资源执行响应的任务</mark>

    自旋

    我们接着查看Unsafe类中的getAndAddInt方法源码

    • var1代表当前对象this
    • var2代表内存偏移值
    • var4是常量1

    var5 = this.getIntVolatile(var1, var2);调用类Unsafe类的一个本地方法getIntVolatile获取当前对象这个内存偏移量的值是多少,获取到后赋值给var5。

    this.compareAndSwapInt(var1, var2, var5, var5 + var4)接着又调用Unsafe类的compareAndSwapInt本地方法。var代表当前对象,var2代表内存偏移量。方法作用就是当前对象的内存偏移量位置取到的值是否和我们先前取到的var5值相同,如果相同值就变成了var5+var4,也就是值加1。方法成功返回true取否为flase,从而退出循环!返回var5以前的值。

    如果当前对象这个内存偏移量的值与我们先前取到的var5不相同,则不加1返回false取否变为true,再次循环,直到比较成功而更新值,返回以前的值!这就是自旋!

    • var1 Atomic Integer 对象本身
    • var2 该对象值的引用地址
    • var4需要变得数量
    • var5是用var1,var2找出的主内存中真实的值。

    用该对象当前的值与var5比较:

    • 如果相同,更新var5+var4并且返回true
    • 如果不同,继续取值然后再比较,直到更新完成。

    假设线程A和线程B两个线程同时执行getAndAddInt操作(分别在不同的CPU上):

    1. AtomicInteger里面的value原始值为3,即主内存中AtomicInteger的value为3,根据JMM模型,线程A和线程B各自持有一份值为3的value的副本分别到各自的工作内存.

    2. 线程A通过getIntVolatile(var1,var2) 拿到value值3,这是线程A被挂起.

    3. 线程B也通过getIntVolatile(var1,var2) 拿到value值3,此时刚好线程B没有被挂起 并执行compareAndSwapInt方法比较内存中的值也是3 成功修改内存的值为4 线程B打完收工 一切OK.

    4. 这是线程A恢复,执行compareAndSwapInt方法比较,发现自己手里的数值和内存中的数字4不一致,说明该值已经被其他线程抢先一步修改了,那A线程修改失败,只能重新来一遍了.

    5. 线程A重新获取value值,因为变量value是volatile修饰,所以其他线程对他的修改,线程A总是能够看到,线程A继续执行compareAndSwapInt方法进行比较替换,直到成功.

    CAS缺点

    循环时间长开销很大

    CAS 通常是配合无限循环一起使用的,我们可以看到 getAndAddInt 方法执行时,如果 CAS 失败,会一直进行尝试。如果 CAS 长时间一直不成功,可能会给 CPU 带来很大的开销。

    只能保证一个变量的原子操作

    当对一个变量执行操作时,我们可以使用循环 CAS 的方式来保证原子操作,但是对多个变量操作时,CAS 目前无法直接保证操作的原子性。但是我们可以通过以下两种办法来解决:

    1. 使用互斥锁来保证原子性;
    2. 将多个变量封装成对象,通过 AtomicReference 来保证原子性。

    只能保证一个变量的原子操作也就是var1

    ABA问题

    相关文章

      网友评论

          本文标题:JUC之CAS原理

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