• CAS是什么?ABA问题又应该如何理解?


    一、为什么值和预期不一样?

    ​ 我们先来看下下面的这段 Java 程序,开启十个线程,每个线程进行 number++ 操作 1000 次,最终输出的值大小应该为 10000:

    public void addNumber(){
        number++;
    }
    
    for (int i = 1; i <=10 ; i++) {
        new Thread(()->{
            for (int j = 1; j <=1000; j++) {
                data.addNumber();
            }
        },String.valueOf(i)).start();
    }
    

    ​ 当我们打印出最终的 number 的值的时候发现,每一次的值都小于预期的 10000 。这是因为 number++ 并非是一步操作,当执行它时会分为三条指令:① 获取到原始值;② 对原始值进行加一操作得到新值;③ 将新值写回内存。在并发较高的情况下,当两个线程同时获取到旧的值之后,就会产生写入的值相同的状况,造成总和总比预期值小的后果。

    ​ 为了解决这个问题,我们可以给 addNumber() 方法加上 synchronized 修饰解决。synchronized 属于悲观锁,一次只允许一个线程进行 number++的操作,虽然这样能够解决并发问题,但是在此处的效率并不高。由此我们可以使用一种乐观锁,乐观锁的含义是假设没有发生冲突,那么就正好可以进行某项操作,如果要是发生冲突了,那就重试直到成功,其最常见的就是CAS 。对 addNumber()方法进行以下修改便可以避免并发值重复的问题。

    AtomicInteger atomicInteger =new AtomicInteger();
    public void addNumber(){
        atomicInteger.getAndIncrement();
    }
    

    ​ 这里边使用到了java.util.concurrent.atomic包下的AtomicInteger来解决原子性问题。

    二、CAS是什么?

    ​ 在上面我们使用到了 Java 的java.util.concurrent.atomic包,这个包便借用了 CAS 来实现了区别于synchronized 同步锁的一种乐观锁。

    ​ 那么 CAS 是什么呢?CAS其实就是Compare And Swap 的简写,它是一条 CPU 并发原语。原语的执行必须是连续的,在执行过程中不允许中断,也就是说CAS是一条原子指令,不会造成所谓的数据不一致的问题。我们跟踪atomicInteger.getAndIncrement();这条语句可以得到以下的内容:

    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }
    

    ​ 由此可以发现它的实现借助了一个叫做Unsafe的类。Unsafe类是 Java 中用于直接操作内存数据的一个类(类似于C语言中的指针操作),其中包含很多的本地方法(native)。

    ​ 以getAndAddInt(Object var1, long var2, int var4)方法为例,展示其实如何实现的保证原子性。方法的实现如下:

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

    ​ 该方法中包含三个参数,分别代表的意思是:

    ​ ① 当前对象;

    ​ ② 该变量值在内存中的偏移地址;

    ​ ③ 需要增加的值大小。

    ​ 在这段代码中,有一个叫做compareAndSwapInt的方法,public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);,该方法共包含四个参数,返回值为布尔型,使用当前对象的当前值与 var5 进行比较,如果相同则更新值返回 true,失败返回 false,参数含义如下。

    ​ Object var1:代表当前对象;

    ​ long var2:代表内存偏移量,相当于对象值的引用地址;

    ​ int var4:代表期望值,使用期望值和当前对象中的值进行比较;

    ​ int var5:代表要交换的值。

    ​ 该方法中使用了自旋锁以保证其原子性。假设主内存值为 v 等于10,此时有 T1、T2两个线程进入到该方法,根据 Java 内存模型(JMM)我们可以知道,线程 T1 和线程 T2 都会将主内存的值10拷贝到自己的工作内存。

    ​ 1、当线程 T1 和线程 T2 都通过getIntVolatile(var1, var2)赋值给了变量 var5 之后,线程 T1 被挂起;

    ​ 2、线程 T2 调用方法compareAndSwapInt,因为当中的期望值 var5 和当前主内存值相同,比较成功,更新当前内存的值为 11,返回 true,退出循环;

    ​ 3、线程 T1 被唤醒,在执行compareAndSwapInt方法的时候,由于当前内存的值以及为11,和 工作内存 var5 的值10不同了,所以比较不成功,返回 false,继续执行循环;

    ​ 4、线程 T1 重新从主内存获取当前的最新值11赋值给 var5;

    ​ 5、线程 T1 继续进行比较,若此时没有其他线程对主内存的进行修改,比较更新成功 ,退出循环;否则继续执行步骤4。

    ​ 流程图如下所示:

    ​ 虽然CAS没有加锁保证了一致性,并发性有所提高 ,但是也产生了一系列的问题,比如循环时间长开销大只能保证一个共享变量的原子操作会产生ABA问题

    三、ABA 问题是什么?

    ​ 使用 CAS 会产生 ABA 问题,这是因为 CAS 算法是在某一时刻取出内存值然后在当前的时刻进行比较,中间存在一个时间差,在这个时间差里就可能会产生 ABA 问题。

    ​ ABA 问题的过程是当有两个线程 T1 和 T2 从内存中获取到值A,线程 T2 通过某些操作把内存 值修改为B,然后又经过某些操作将值修改为回值A,T2退出。线程 T1 进行操作的时候 ,使用预期值同内存中的值比较,此时均为A,修改成功退出。但是此时的A以及不是原先的A了,这就是 ABA 问题,如下图。

    四、如何解决 ABA 问题?

    ​ 解决这个问题可以使用添加版本号的方式。我们可以使用 Java 中的提供的类AtomicStampedReference进行操作,其中的compareAndSet方法如下:

    public boolean compareAndSet(V   expectedReference,
                                 V   newReference,
                                 int expectedStamp,
                                 int newStamp) {
        Pair<V> current = pair;
        return
            expectedReference == current.reference &&
            expectedStamp == current.stamp &&
            ((newReference == current.reference &&
              newStamp == current.stamp) ||
             casPair(current, Pair.of(newReference, newStamp)));
    }
    

    ​ 这个方法包含了四个参数,expectedReference代表的是期望被修改的值,newReference代表的是新的值,expectedStamp代表期望被修改的版本号,newStamp代表新的版本号。只有当预期值和均当前内存值相同时才会修改成功。ABA 问题的完整示例以及解决代码如下:

    public class ABADemo {
        private static AtomicStampedReference<Integer> stampedReference = new AtomicStampedReference<>(100, 1);
    
        public static void main(String[] args) {
            new Thread(() -> {
                int stamp = stampedReference.getStamp();
                System.out.println("当前线程名称:" + Thread.currentThread().getName() + ",版本号为" + stamp + ",值是" + stampedReference.getReference());
                //暂停1秒钟t1线程
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
    
                stampedReference.compareAndSet(100, 101, stampedReference.getStamp(), stampedReference.getStamp() + 1);
                System.out.println("当前线程名称:" + Thread.currentThread().getName() + ",版本号为" + stampedReference.getStamp() + ",值是" + stampedReference.getReference());
                stampedReference.compareAndSet(101, 100, stampedReference.getStamp(), stampedReference.getStamp() + 1);
                System.out.println("当前线程名称:" + Thread.currentThread().getName() + ",版本号为" + stampedReference.getStamp() + ",值是" + stampedReference.getReference());
                System.out.println("线程t1已完成1次ABA操作~~~~~");
            }, "t1").start();
    
            new Thread(() -> {
                int stamp = stampedReference.getStamp();
                System.out.println("当前线程名称:" + Thread.currentThread().getName() + ",版本号为" + stamp + ",值是" + stampedReference.getReference());
                //线程2暂停3秒,保证线程1完成1次ABA
                try {
                    TimeUnit.SECONDS.sleep(3);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                boolean result = stampedReference.compareAndSet(100, 6666, stamp, stamp + 1);
                System.out.println("当前线程名称:" + Thread.currentThread().getName() + ",修改成功否:" + result + ",最新版本号" +
                        stampedReference.getStamp() + ",最新的值:" + stampedReference.getReference());
            }, "t2").start();
        }
    }
    
  • 相关阅读:
    java解析xml的几种方式
    Android Studio 问题解决List
    Android 无线调试方法
    Android 单选/复选控件
    clr
    jquery.metadata.js使用分析
    更改新建Asp.net WebForm的模板 的方法
    获取定位数据
    简易水平仪实现
    简易指南针实现
  • 原文地址:https://www.cnblogs.com/Mhang/p/12868307.html
Copyright © 2020-2023  润新知