zoukankan      html  css  js  c++  java
  • JUC:理解CAS、CAS会出现的ABA问题、解决ABA原子引用、使用Synchronized会比CAS操作慢效率低


    深入理解CAS JDK1.5

    在计算机科学中,比较和交换(Conmpare And Swap)是用于实现多线程同步的原子指令。 它将内存位置的内容与期望值进行比较,只有在相同的情况下,将该内存位置的内容修改为新的给定值。 这是作为单个原子操作完成的。 原子性保证新值基于最新信息计算; 如果该值在同一时间被另一个线程更新,则写入将失败。 操作结果必须说明是否进行替换; 这可以通过一个简单的布尔响应(这个变体通常称为比较和设置),或通过返回从内存位置读取的值来完成。

    JAVA1.5开始引入了CAS,主要代码都放在JUC的atomic包下。

    • CAS(compareAndSet比较并设置方法)在java层面是:

    在这里插入图片描述

    通过源码:

    在这里插入图片描述

    如果这个类的期望值(此刻的值)是0,那么就将更新的值赋值给这个类,成为该类的最新值,成功赋值返回true,否则返回false。

    • CAS(compareAndSwapInt比较并设置方法)在底层是:

    Unsafe类是java底层源码,native修饰的,通过C++语言,通过原语对cpu进行操作。原语是若干个指令组成,并且必须是连续执行来完成一个任务,过程中不能被其他线程插入。

    通过AtomicInteger atomicInteger = new AtomicInteger(0);atomicInteger.getAndIncrement();方法,得知源码

    在执行CAS语句时:var1,var2获得内存此刻的值,与之前获得的内存值(期望值)var5,进行比较,比较相同,则进行var5=var5+var4并返回var5

    var1为当前对象

    var2为内存偏移量

    var4为1

    var5为获取当前对象var1和内存偏移量var2在内存中的值

    我们看到有一个do while去比较更新,如果内存中值和当前值相同则新增1,不相同在接着比较。从而我们也可以知道CAS的缺点:如果CAS失败就会一直进行尝试,长时间不成功,可能会跟CPU带来很大的开销。

    CAS会出现ABA问题

    如线程1从内存X中取出A,这时候另一个线程2也从内存X中取出A,并且线程2进行了一些操作将内存X中的值变成了B,然后线程2又将内存X中的数据变成A,这时候线程1进行CAS操作发现内存X中仍然是A,然后线程1操作成功。虽然线程1的CAS操作成功,但是整个过程就是有问题的。

    在这里插入图片描述

    解决ABA 原子引用

    所以JAVA中提供了AtomicStampedReferenceAtomicMarkableReference原子引用来处理会发生ABA问题的场景,主要是在对象中额外再增加一个标记来标识对象是否有过变更。

    - AtomicStampedReference遇到的坑

    AtomicStampedReference<Integer> stampedReference = new AtomicStampedReference<>(1,1);
    // 第一个参数值是Reference值,就是需要更新的Interge类型的值
    // 第二个参数值是stamp值,用于记录版本信息,用于解决ABA的问题

    大坑就在此,AtomicStampedReference的构造器:

    public AtomicStampedReference(V initialRef, int initialStamp) {
        pair = Pair.of(initialRef, initialStamp);
    }

    这里的Pair是AtomicStampedReference的内部类,用于存储更新值和邮票值,存储的Integer类型的数字在java中是有缓存的,缓存范围是-128~127,所以当AtomicStampedReference类初始化为这个之间的数字时,自动从缓存中获取,因此地址也是引用的缓存中的地址。在后续进行比较交换时:

    stampedReference.compareAndSet(
                        1, 
                        2, 
                        stampedReference.getStamp(), 
                        stampedReference.getStamp() + 1);

    期望值是1,1在缓存中,会引用缓存中的内存地址,并且初始时也是1,期望值于当前值的引用地址都是一样的,compareAndSet方法通过==进行判断值期望值于当前值是否相等:

    在这里插入图片描述

    因此不会有任何错误,比较交换都会成功返回ture。

    但是:如果你初始化为1130并且更新值时,也是输入的1130为期望值:

    AtomicStampedReference<Integer> stampedReference = new AtomicStampedReference<>(1130,1);
    // 更新值方法
    stampedReference.compareAndSet(
                        1130, 
                        2, 
                        stampedReference.getStamp(), 
                        stampedReference.getStamp() + 1);

    这种结果会出错,会返回false。这时由于当初始化时,传入的1130已经超过了缓存值,会创建一个新的内存空间存储这个Integer类型的值,而当调用compareAndSet时,也传入了1130,这个也已经超过了缓存值,因此又会创建一个内存空间存储这个Integer值,由于compareAndSet方法是通过==进行比较的,不是通过equals进行比较值的,因此期望值于当前值的引用地址不一样导致无法更新值成功,所以会返回false。

    解决办法:

    既然知道原因了,那么就很好解决了,在更新值的时候,直接使用stampedReference.getReference()方法获得当前值作为期望值就可以了。这样期望值和当前值的引用地址都一样,可以确保交换成功!,此时就返回ture了。

    stampedReference.compareAndSet(
                        stampedReference.getReference(),
                        2,
                        stampedReference.getStamp(),
                        stampedReference.getStamp() + 1)

    实例:

    public class Test01{
        public static void main(String[] args) throws Exception {
            AtomicStampedReference<Integer> stampedReference = new AtomicStampedReference<>(1,1);
            int stamp = stampedReference.getStamp(); // 获取当前邮票值 为 1
    
            new Thread(()->{
                // 对值进行CAS操作,并将邮票更新
                System.out.println("A "+stampedReference.compareAndSet(
                        1, 
                        2, 
                        stampedReference.getStamp(), 
                        stampedReference.getStamp() + 1));
                System.out.println("a1 => "+stampedReference.getStamp());
                // 对值进行CAS操作,并将邮票更新
                System.out.println("A "+stampedReference.compareAndSet(
                        2, 
                        1, 
                        stampedReference.getStamp(), 
                        stampedReference.getStamp() + 1));
                System.out.println("a2 => "+stampedReference.getStamp());
            }).start();
    
    
            new Thread(()->{
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 对值进行CAS操作,期望值与内存值相同,会更新为3。但是内存邮票值与期望邮票值不同,所以整个CAS操作失败。
                // 因此可以解决ABA的问题,就不会出现原有的ABA的问题了。
                System.out.println("B "+stampedReference.compareAndSet(
                        1, 
                        3, 
                        stamp, 
                        stampedReference.getStamp() + 1));
                System.out.println("b1 => "+stampedReference.getStamp());
            }).start();
        }
    }

    输出:

    A true
    a1 => 2
    A true
    a2 => 3
    B false
    b1 => 3

    该原子引用,虽然使用了ABA的方式,线程A将原来的值赋了新值之后又将其赋值到原来的值,但是线程B操作的时候,会发现邮票值有变动,因此会操作失败。解决了ABA的问题。

    CAS实现原子操作3大问题

    • ABA问题
    • CAS执行时间长,会消耗CPU资源,开销大
    • CAS操作只能执行保证一个共享变量

    使用synchronized会比CAS操作慢和效率低

    • (悲观锁) 使用synchronized关键字,只允许一个线程进入同步代码块进行操作,而其他线程就必须在同步代码块外等待,此时其他线程就进入了BLOCKED状态,不占用CPU资源。而当前线程操作完之后,会释放锁,其他线程会去抢锁,如果抢到了就会被掉到CPU中进行执行。很明显,使用synchronized的同步代码,线程会在阻塞状态进入运行状态之间切换,有一个上下文切换的过程,该过程会浪费时间。

      上下文切换:

      同步代码中,一个线程拿到锁从阻塞状态进入运行状态的时候,就被调度CPU中执行,一个线程除了去执行代码之外,线程本身也是有一些数据的,这些数据会调度到CPU cache中;而执行完之后,会从运行状态进入阻塞状态或是其他状态,这时就会将该线程的数据和线程从CPU中拿出去,换下一个进程进来。这样个过程就是上下文切换。

    • (乐观锁) 使用CAS操作,CAS是比较并交换,因为CAS操作时原子性的,始终只会存在一个线程去执行CAS,所以不会存在线程安全问题。那么就会使得所有线程去获操作的时候,都会去比较,如果比较正确就执行交换,如果不正确就再次循环去拿新的值。当一个线程在CAS操作时,其他线程都会一直循环,空轮训,所有线程一直都是在执行中,少了上下文的切换。所以CAS操作效率会高很多,执行速度会快很多。

    总结: 由于synchronized在线程调度上有上下文切换,浪费很多时间,而CAS操作的每一个线程都是一直循环然后比较值,没有上下文的切换,所以CAS会快很多。

  • 相关阅读:
    Shell面试题4:扫描网络内存活主机案例
    第三题批量创建特殊要求用户案例
    chpasswd 更简单的更改密码的方式
    30题之第二题
    shell30题之第一题
    2019牛客多校第四场B xor——线段树&&线性基的交
    POJ 2893 M × N Puzzle——八数码有解条件
    2019HDU多校第四场 Just an Old Puzzle ——八数码有解条件
    2019HDU多校第三场F Fansblog——威尔逊定理&&素数密度
    威尔逊定理
  • 原文地址:https://www.cnblogs.com/turbo30/p/13688201.html
Copyright © 2011-2022 走看看