深度理解CAS和ABA问题

一》对于CAS的理解

要对CAS进行探究,我们先从AtomicInteger这个类的getAndIncrement()这个方法说起 ,这个方法主要可以解决volatile关键字不保证原子性的问题。下面我们进入这个方法中进行进行探究:

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

进入getAndIncrement()这个方法,可以看到底层调用了Unsafe这个类对象的getAndAddInt()方法,再进入Unsafe这个类:

 public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);

    public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

    public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);

    public native Object getObjectVolatile(Object var1, long var2);

    public native void putObjectVolatile(Object var1, long var2, Object var4);

Unsafe这个类来自JVM里rt.jar这个包,是JVM自身携带的一个类,可以看到里边的方法都是native关键字修饰的,说明Unsafe这个类的方法都是调用操作系统底层的资源来执行相应的任务的,类似于C语言中的指针操作内存。getAndIncrement()这个方法之所以能保证原子性,就是Unsafe这个类起到了作用。

再进入getAndAddInt(this, valueOffset, 1)这个方法:

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

这段代码就是CAS操作的核心,可以看到主要是通过循环来实现的,先来看看几个核心参数:

var1:表示当前的对象;

var2 :表示地址偏移量(Unsafe就是通过地址偏移量来获取数据的);

var5 :表示主物理内存中真实的共享变量的值;

var4+var5:表示变化量

首先通过var1和var2来获取主物理内存中的真实值var5,再与当前对象的值进行比较,如果相等,更新var4+var5,返回true,跳出while循环,如果不相等,继续进行取值和比较,直到相等为止。

既然Unsafe这个类是通过偏移量进行获取数据的,那么这个偏移量是怎么计算的呢?

 static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    private volatile int value;

可以看到底层的value值用volatile关键字修饰,这样就保证了共享变量的可见性。

讲到这里,那么什么是CAS呢,所谓的CAS,就是一个比较并交换的过程,它是通过自旋锁的方式进行线程安全的保证。它先将预期值和真实值进行比较,看是否相等,相等的话,就进行修改,这个过程是原子性的。CAS是一条并发原语,是CPU的原子指令,依赖于硬件的物理功能,执行过程不会被打断,就不会存在数据不一致的问题。

讲到这里,大家可能还是不太懂,下面就通过一个具体的实例来体会CAS的原理:

  public static void main(String[] args) {
        AtomicInteger atomicInteger=new AtomicInteger(5);
        //先将主物理内存中的5改为2019
        boolean b = atomicInteger.compareAndSet(5, 2019);
        //再将主内存中的值改回5
        boolean b1 = atomicInteger.compareAndSet(5, 5);
        System.out.println(b+"\t"+atomicInteger.get());
        System.out.println(b1+"\t"+atomicInteger.get());

    }

运行结果:

主物理内存里的共享变量的值为5,调用compareAndSet()方法进行比较并交换,通过比较,主物理内存中的值和期望的值是相等的,这时候交换成功,返回true,主物理内存中的值被换成2019。但是当重新改回5的时候,返回是false,说明修改失败,主内存中的值依然是2019,下面通过图示的方法分析一下这个修改失败的原因。

深度理解CAS和ABA问题_第1张图片

 先来对这个JMM内存模型进行一个描述:

由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成。

第一次修改之后,主内存中的共享变量的真实值变成了2019,当第二次想改回来的时候,将期望值5和主内存中的值2019进行比较,发现二者不相等,所以修改失败。

二》CAS的缺点

CAS虽然能保证线程安全性和并发性,但还有缺点:

1.自旋的时间过长,消耗资源;

2.只能保证一个共享变量的原子性;

3.会产生ABA问题

三》ABA问题

ABA问题就是,一个线程修改了主物理内存中的值,又将值改为原来的值,这时候另一个线程来进行比较,以为值没有被修改,但实际被修改过,类似于狸猫换太子。

深度理解CAS和ABA问题_第2张图片

比如线程T1执行时间需要10s,线程T2执行时间是2s,线程T2比线程T1快,加入主物理内存中的共享变量的值是A,T2经过比较,跟自己的工作内存中的值是一致的,这时候就把值改为B并放到主物理内存中,由于T2执行速度快,这时候又把值改为A,这时T1线程执行,自己工作内存中的值和主物理内存中的值相同,以为没被改过,但实际上被动过,类似于狸猫换太子

在学习ABA问题之前,我们先来了解一下原子引用。

在前面我们提到了原子整形AtomicInteger,它放进主物理内存的值只能是整形,但是当我们想把自定义对象放进主物理内存,就要用到原子引用。

 public static void main(String[] args) {
        User user=new User(1,"zhangsan");
        AtomicReference atomicReference=new AtomicReference<>();
        //将user对象放进了主物理内存
        atomicReference.set(user);
        System.out.println(atomicReference.get());

运行结果:

 可以看到,通过原子引用,我们成功的把自定义的对象放进了主物理内存。

那么ABA问题该怎么解决呢?这里我们需要用到版本号原子引用AtomicStampedReference。

定义一个版本号,版本号的作用就是,当线程对主物理内存中的值改变时,每改变一次,就让版本号加1,当发生ABA问题,另一个线程进行比较的时候,虽然主物理内存中的值和期望值一致,但是版本号不一致,这时,就不能成功修改,防止了ABA问题。

下面看一个实例:

   //版本号默认是1,主内存的值是100
       AtomicStampedReference atomicStampedReference=new
               AtomicStampedReference<>(100,1);
       //创建线程a
        new Thread(
                ()->{
                    int tamp=atomicStampedReference.getStamp();//初始版本号
                    System.out.println(Thread.currentThread().getName()+"第一次版本号为: "+tamp);
                    //暂停一秒,让b线程读取初始版本号
                    try {
                        TimeUnit.SECONDS.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    //让a线程进行一次ABA操作
                    atomicStampedReference.compareAndSet(100,
                            101,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
                    System.out.println(Thread.currentThread().getName()+"第二次版本号为: "+atomicStampedReference.getStamp());
                    //在将2019改回100,版本号+1
                    atomicStampedReference.compareAndSet(101,100,
                            atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
                    System.out.println(Thread.currentThread().getName()+"第三次版本号为: "+atomicStampedReference.getStamp());
                },"a"
        ).start();

        //创建线程b
        new Thread(()->{
            int tamp=atomicStampedReference.getStamp();//初始版本号
            //暂停4秒,让a线程进行一次ABA操作
            try {
                TimeUnit.SECONDS.sleep(4);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"第一次版本号为: "+tamp);
            //b线程进行值的修改,版本号+1
            atomicStampedReference.compareAndSet(100,
                    2019,tamp,atomicStampedReference.getStamp()+1);
            System.out.println(Thread.currentThread().getName()+"第二次版本号为: "+atomicStampedReference.getStamp());
            System.out.println(atomicStampedReference.getReference());

        },"b").start();

    }

运行结果:

深度理解CAS和ABA问题_第3张图片

b线程的期望版本号是2,因为修改了一次,但是a线程进行ABA操作,修改了两次,实际版本号是3,版本号不相同,b线程修改值失败,依然是100。 

你可能感兴趣的:(java,intellij-idea)