18.AtomicReference、AtomicStampReference底层原理。多个变量更新怎么保证原子性?CAS的ABA问题怎么解决?

2023年 10月 6日 36.0k 0

老王:小陈啊,上一章我们说了AtomicInteger、AtomicBoolean的底层原理,这一篇我们就来说说Atomic系列的另一个分类AtomicReference和AtomicStampReference。

小陈:老王啊,我有个疑问啊,java不是提供了AtomicInteger、AtomicBoolean这些原子类了吗?为什么还需要有AtomicReference这东西啊?

老王:JUC虽然提供了AtomicInteger、AtomicBoolean这些基本类型的原子类,但是啊有些场景并不是仅仅修改一个变量那么简单,有可能某个需要修改几个变量,但是需要这个操作具有原子性,比如说我给你举例的这个例子:

(1)假如有三个变量,value1、value2、value3,我需要他们都两两相等

(2)这时将value1、value2、value3都声明成AtomicInteger原子类

(3)定义一个线程类,创建两个线程实例,每个都执行5000次value1、value2、value3的操作

(4)每次操作完成之后对比value1、value2、value3是否两两相等,如果不满足,则打印报错

public class MultiUpdateDemo {
    // 声明三个AtomicInteger的原子类
    private static AtomicInteger value1 = new AtomicInteger(0);
    private static AtomicInteger value2 = new AtomicInteger(0);
    private static AtomicInteger value3 = new AtomicInteger(0);
    // 定义一个线程,执行3个AtomicInteger的++操作
    public static class MultiUpdateThread extends Thread{
        @Override
        public void run() {
            for (int i = 0; i < 5000; i++) {
                value1.incrementAndGet();
                value2.incrementAndGet();
                value3.incrementAndGet();
                // 假如说执行完一次操作之后,出现
                // value1、value2、value3任何两两不相等的情况
                // 则打印报错
                if (value1.get() != value2.get() || value1 != value3
                        || value2.get() != value3.get()) {
                    System.out.println("不好意思,出错了!!!!!!");
                }
            }
        }
    }
 
    public static void main(String[] args) throws InterruptedException {
        // 创建两个线程,并发的操作
        MultiUpdateThread thread1 = new MultiUpdateThread();
        MultiUpdateThread thread2 = new MultiUpdateThread();
 
        thread1.start();
        thread2.start();
 
        thread1.join();
        thread2.join();
    }
}

具体得到的实验结果如下:

image.png

老王:根据样例的实验结果啊,出现和很多次value1、value2、value3不相等的情况,也就是我们需要的目的没有达到,小陈啊,你知道这是什么原因吗?

小陈:老王啊,我想了一下单独对value1、value2、value3中任意一个执行incrementAndGet是原子的;但是value1.incrementAndGet()、 value2.incrementAndGet()、value3.incrementAndGet()这三个操作合起来就不是原子的。

可能thread1执行value1.incrementAndGet()操作的时候,thread2已经将三个自增操作执行完了,所以啊,thread1和thread2会相互干扰......

老王:哈哈,小陈啊,看来我没看错你啊,挺聪明的哦。像是这种情况啊要对多个变量进行操作,同时又要保证这个操作具有原子性,单独使用AtomicInteger、AtomicBoolean是做不到的。

小陈:老王,如果使用到锁不可以吗,比如我可以将上面的几个操作放到synchronized代码块里面:

 // lock锁对象是一个共享变量
 synchronized(lock) {
    value1.incrementAndGet();
    value2.incrementAndGet();
    value3.incrementAndGet();
    // 加入说执行完一次操作之后,出现value1、value2、value3任何两两不相等的情况
    if (value1.get() != value2.get() || value1 != value3
            || value2.get() != value3.get()) {
        System.out.println("不好意思,出错了!!!!!!");
    }
}

老王:这种情况下使用synchronized是可以保证原子性的,但是使用到锁啊,那并发性能就下降了很多了,因为在竞争激烈的时候可能会导致很多线程获取不到锁而挂起,那开销就大了,这个我们在之前的synchronized的重量级锁章节里面专门分析过了。

小陈:哦哦,原来是这样啊......

老王:嗯嗯,AtomicIntegter只能确保自己本身操作具有原子性,但是多个AtomicInteger操作合起来这个是确保不了的;可以使用synchronized将多个操作包含起来,但是使用到synchronized的锁操作势必会降低一部分并发的性能。

小陈:那怎样在不使用锁的情况下保证多个变量的修改是具有原子性的呢?

老王:哈哈,这个时候就需要用到Atomic给我们提供的另外一个类了,AtomicReference。它可以将多个变量封装为对象的多个属性,然后一次性的更新整个对象,就能cas的更新多个变量,确保原子性。

AtomicReference实现一个对象原子更新

public class ReferenceDemo {
    // 声明一个AtomicReference,封装Demo对象的
    private static AtomicReference reference = new AtomicReference(new Demo());
    // 将value1、value2、value3封装为Demo对象的属性
    public static class Demo {
        public  int value1 = 0;
        public  int value2 = 0;
        public  int value3 = 0;
    }
    // 创建线程累专门执行对象的更新
    public static class ReferenceThread extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < 5000; i++) {
                Demo expected;
                Demo update;
                // 直到CAS更新操作成功才退出
                do {
                    expected = reference.get();
                    update = new Demo();
                    update.value1 =  expected.value1 + 1;
                    update.value2 =  expected.value2 + 1;
                    update.value3 =  expected.value2 + 1;
                } while (!reference.compareAndSet(expected, update));
 
                // 获取CAS之后的最新对象
                Demo curDemo = reference.get();
                // 如果value1、value2、value3中有任意一个不相等,打印报错
                if (curDemo.value1 != curDemo.value2 || curDemo.value2 != curDemo.value3
                    || curDemo.value1 != curDemo.value3) {
                    System.out.println("不好意思,出错了!!!!!!");
                }
 
            }
        }
    }
 
    public static void main(String[] args) throws InterruptedException {
        // 创建两个线程,并发的操作,验证并发操作的原子性
        ReferenceThread thread1 = new ReferenceThread();
        ReferenceThread thread2 = new ReferenceThread();
 
        thread1.start();
        thread2.start();
 
        thread1.join();
        thread2.join();
 
        System.out.println("运行结束了......");
    }

实测结果如下:

image.png

并没有打印报错信息。它这里啊相当于把value1、value2、value3的更新操作变为了对象的更新操作,这样原本的3次操作就变为了一次CAS操作,这样就能保证原子性了。

小陈:原来是这样啊,原来是多个数据变更的操作变为一个对象变更操作;由于AtomicReference提供了对象替换的CAS操作,所以上面的操作就具有原子性了。

老王:是的,就是这个道理,画个图来解析它的步骤,就是这样的:

image.png

(1)将多个变量封装在一个对象中,比如demo对象,封装了value1、value2、value3变量的值,此时三个变量均为0

(2)此时要将3个变量的值均更新为1,则新创建一个对象update封装value1、value2、value3的值均为1

(3)此时只需要将旧的demo对象通过cas操作替换为新的update对象即可,这样就将多个变量的更新操作变为了一个对象的cas替换操作。

老王:让我们继续,来看看AtomicReference底层有什么东西?

AtomicReference原子类底层剖析

首先看一下AtomicReference的内部属性:

public class AtomicReference implements java.io.Serializable {
    // unsafe对象
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    // 一个泛型对象
    private volatile V value;
    // value对象在AtomicReference内部的偏移量
    private static final long valueOffset;
    static {
        try {
            // 获取value相对AtomicReference的内部偏移量
            valueOffset = unsafe.objectFieldOffset
                (AtomicReference.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }    
}

看下compareAndSet方法的内部源码:

public final boolean compareAndSet(V expect, V update) {
    return unsafe.compareAndSwapObject(this, valueOffset, expect, update);
}

看样子跟Atomicinteger和AtomicBoolean原理是一样的,只不过AtomicInteger、AtomicBoolean底层调用的是unsafe.compareAndSwapInt方法CAS操作int的值,而这里是compareAndSwapObject是CAS操作一个内存对象而已,没啥大区别。

小陈:看样子这个AtomicReference也是挺简单的呀,唯一与AtomicInteger、AtomicBoolean不同的是:

AtomicInteger、AtomicBoolean 执行的是unsafe的compareAndSwapInt方法,在内存层次是直接替换一个int变量的值;然而使用AtomicRefernce你可以创建一个新的对象,将所有的数据变更操作放到新对象里面,然后底层调用unsafe.compareAndSwapObject方法直接替换成新对象啊。

老王:哈哈,看来你把AtomicReference理解透了啊?也是,本来就不难嘛,就是把多个修改放在对象里面,直接CAS替换对象就是了

老王:小陈啊,学到了这里,你对CAS的理解也差不多了,可以算的是深入了,但是你知道CAS操作会有什么问题吗?

小陈:这个啊,我经常看到一些网站上说CAS操作不可避免的问题之一就是ABA问题?

老王:那你知道什么是ABA问题?

小陈:老王啊,我画个图说一下我对ABA问题的理解吧,是这样的:

(1)线程1要执行CAS操作前,读取value最新的值为A

(2)然后线程2在这期间将内存value的数据修改成B,然后又修改回了A;

(3)但是线程A不知道,执行CAS操作的时候发现值还是A,以为没人修改过value的值,也是就执行执行CAS操作成功了

image.png

老王:那应该怎么避免ABA这种问题?

小陈:这个应该是多增加一个维度,比如版本号,每一次修改数据版本号则递增1,然后执行CAS操作的时候多一个版本号维度判断,这样就能避免ABA问题了。

老王:是的,确实是需要多一个版本号维度去判断,那你知道Atomic原子类里面哪个类能解决这个问题吗?

小陈:啊,这个......我就不知道啊,还是老王你来讲讲吧。

老王:哈哈,好。Atomic原子类系列里面有一个类叫做AtomicStampedReference,是AtomicReference的升级版本,看名字你就知道多了一个叫做Stamped的东西,这东西就是版本号,也叫作邮戳。下面让我们看看AtomicStampedReference的内部结构和核心方法。

AtomicStampedReference

内部结构:

public class AtomicStampedReference {
    // 将当前对象引用和修改的版本号绑定成一个pair对
    private static class Pair {
        // 对象引用
        final T reference;
        // 版本号
        final int stamp;
        private Pair(T reference, int stamp) {
            this.reference = reference;
            this.stamp = stamp;
        }
        static  Pair of(T reference, int stamp) {
            return new Pair(reference, stamp);
        }
    }
    private volatile Pair pair;
}    

这里比较上面的AtomicReference多了一个stamp版本号,将对象和版本号绑定在一起,形成一对pair,比较的时候同时比较对象的引用和版本号,避免ABA问题。

核心执行修改的CAS方法:

public boolean compareAndSet(V   expectedReference,
                             V   newReference,
                             int expectedStamp,
                             int newStamp) {
    // 这里获取当前的版本号和对象                             
    Pair current = pair;
    return
        // 这里对比对象是否被修改过,如果被修改过,则对象引用变化了
        expectedReference == current.reference &&
        // 比较版本号是否一致
        expectedStamp == current.stamp &&
        ((newReference == current.reference &&
          newStamp == current.stamp) ||
         casPair(current, Pair.of(newReference, newStamp)));
}

(1)获取旧的对象引用expectedRefenence

(2)执行CAS操作前,获取当前内存最新的数据

(3)对比旧的对象和当前对象的reference引用是否同一个,版本号stamp是否相同

(4)如果相同执行CAS操作替换,否则不一样说明有别的线程修改过数据,CAS操作失败

casPair方法:

直接调用底层的unsafe类的compareAndSwapObject方法直接替换一个对象:

private boolean casPair(Pair cmp, Pair val) {
    return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
}

让我们再画一张图来捋一捋:

image.png

老王:小陈啊,AtomicStampedReference的底层原理通过上面的代码和画图讲解,你听懂了没?

小陈:某问题了,其实就是比AtomicReference多了一个版本号stamped,在执行CAS操作之前对比reference的值的同时也对比版本号,如果reference一样但是stamped不一样,说明期间有人修改过但是又把值改回来了,就不允许执行CAS操作了,这样就能解决ABA的问题了。

老王:没错,理解完全正确。

小陈:老王啊,接下来我们学习什么?

老王:接下来啊,我们讲解Atomic系列中的下一章《Atomic系列之LongAdder的底层原理(分段锁提升并发性能) 》

小陈:牛逼plus......,我们下一章见。

目录

JAVA并发专题 《筑基篇》

1.什么是CPU多级缓存模型?

2.什么是JAVA内存模型?

3.线程安全之可见性、有序性、原子性是什么?

4.什么是MESI缓存一致性协议?怎么解决并发的可见性问题?

JAVA并发专题《练气篇》

5.volatile怎么保证可见性?

6.什么是内存屏障?具有什么作用?

7.volatile怎么通过内存屏障保证可见性和有序性?

8.volatile为啥不能保证原子性?

9.synchronized是个啥东西?应该怎么使用?

10.synchronized底层之monitor、对象头、Mark Word?

11.synchronized底层是怎么通过monitor进行加锁的?

12.synchronized的锁重入、锁消除、锁升级原理?无锁、偏向锁、轻量级锁、自旋、重量级锁

13.synchronized怎么保证可见性、有序性、原子性?

JAVA并发专题《结丹篇》

14. JDK底层Unsafe类是个啥东西?

15.unsafe类的CAS是怎么保证原子性的?

16.Atomic原子类体系讲解

17.AtomicInteger、AtomicBoolean的底层原理

18.AtomicReference、AtomicStampReference底层原理

19.Atomic中的LongAdder底层原理之分段锁机制

20.Atmoic系列Strimped64分段锁底层实现源码剖析

JAVA并发专题《金丹篇》

21.AQS是个啥?为啥说它是JAVA并发工具基础框架?

22.基于AQS的互斥锁底层源码深度剖析

23.基于AQS的共享锁底层源码深度剖析

24.ReentrantLock是怎么基于AQS实现独占锁的?

25.ReentrantLock的Condition机制底层源码剖析

26.CountDownLatch 门栓底层源码和实现机制深度剖析

27.CyclicBarrier 栅栏底层源码和实现机制深度剖析

28.Semaphore 信号量底层源码和实现机深度剖析

29.ReentrantReadWriteLock 读写锁怎么表示?

  • ReentrantReadWriteLock 读写锁底层源码和机制深度剖析
  • JAVA并发专题《元神篇》并发数据结构篇

    31.CopyOnAarrayList 底层分析,怎么通过写时复制副本,提升并发性能?

    32.ConcurrentLinkedQueue 底层分析,CAS 无锁化操作提升并发性能?

    33.ConcurrentHashMap详解,底层怎么通过分段锁提升并发性能?

    34.LinkedBlockedQueue 阻塞队列怎么通过ReentrantLock和Condition实现?

    35.ArrayBlockedQueued 阻塞队列实现思路竟然和LinkedBlockedQueue一样?

    36.DelayQueue 底层源码剖析,延时队列怎么实现?

    37.SynchronousQueue底层原理解析

    JAVA并发专题《飞升篇》线程池底层深度剖析

  • 什么是线程池?看看JDK提供了哪些默认的线程池?底层竟然都是基于ThreadPoolExecutor的?
  • 39.ThreadPoolExecutor 构造函数有哪些参数?这些参数分别表示什么意思?

    40.内部有哪些变量,怎么表示线程池状态和线程数,看看道格.李大神是怎么设计的?

  • ThreadPoolExecutor execute执行流程?怎么进行任务提交的?addWorker方法干了啥?什么是workder?

  • ThreadPoolExecutor execute执行流程?何时将任务提交到阻塞队列? 阻塞队列满会发生什么?

  • ThreadPoolExecutor 中的Worker是如何执行提交到线程池的任务的?多余Worker怎么在超出空闲时间后被干掉的?

  • ThreadPoolExecutor shutdown、shutdownNow内部核心流程

  • 再回头看看为啥不推荐Executors提供几种线程池?

  • ThreadPoolExecutor线程池篇总结

  • 相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论