解析AtomicInteger为什么能保证原子性

文章浅谈volatile的最后留下了疑问,为什么AtomicInteger能保证原子性,AtomicInteger是如何做到保证原子性的,本篇文章就是来答疑解惑的。

AtomicInteger源码分析
private static final Unsafe unsafe = Unsafe.getUnsafe();
public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }

分析:这是文章浅谈volatile里所使用的方法源码,可以看到,源码是使用Unsafe类的getAndAddInt()来实现的。那么Unsafe类又是什么样的呢?下图是Unsafe类的部分截图:

在这里插入图片描述
通观Unsafe类的方法,其返回值都是native,也就是c语言的函数,而getAndAddInt()的源码如下图所示:
在这里插入图片描述

源码解析:var1 = this,也就是当前对象,如:num.getAndIncrement();中,this就是num,var2 = valueOffset,也就是偏移量,通过this和偏移量可以准确的在内存中找到改对象的值,也就是var5,var4 = 1,函数getAndIncrement()每次增加1,所以这里是写死的1,compareAndSwapInt(var1, var2, var5, var5 + var4)是CAS方法,含义:比较this的内存值与当前从内存中取出的值var5是否相同,相同则把内存中的值更新为var5+var4(新值),不相同则继续while()循环,直到比较的结果相同为止。

CAS介绍

CAS的全称为Compare-And-Swap,它是一条CPU并发原语。它的功能是判断内存某个位置的值是否为预期值,如果是则更新为新的值,这个过程是和原子的。

CAS并发原语体现在JAVA语言中就是sun.misc.Unsafe类中的各个方法。调用Unsafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令。这是一种完全依赖于硬件的功能,通过它实现了原子操作,不会造成数据不一致问题。

CAS示例
AtomicInteger atomicInteger = new AtomicInteger(2019);
System.out.println("CAS boolean:" + atomicInteger.compareAndSet(2019, 2020) + ",CAS value:" + atomicInteger.get());
System.out.println("CAS boolean:" + atomicInteger.compareAndSet(2019, 2021) + ",CAS value:" + atomicInteger.get());

输出结果:
在这里插入图片描述
结果分析:第一次CAS操作时,期望值与实际值相同,所以更新成功,atomicInteger的值修改为:2020,第二次CAS操作时,期望值(2019)与实际值(2020)不相同,所以比对不成功,也就不会更新(2021)了。

到此其实CAS原理就解释完毕了,但是并没有介绍CAS的缺点,所以接下来会介绍CAS的缺点,以及CAS带来的问题和如何解决问题,帮助更好的理解CAS。

CAS缺点
  • 循环时间长,开销大
  • 只能保证一个共享变量的操作
  • ABA问题

解释:CAS的实现是do-while(),如果某个或者某些线程运气不好,在更新值前,一直被其他线程抢先,那么while()循环会一直执行(自旋),可能对CPU操作开销影响。只能保证一个共享变量这一点很好理解,ABA问题会详细介绍。

ABA问题

CAS比较的时候,是通过期望值和当前值进行比较,如果相等,认为没有被修改,则进行自己的操作,那么当出现,当前值A,某线程X将其修改为B,然后线程X又把当前值改回A,对于另一个线程Y而言,自己获取到的当前值是A,期望值也是A,线程Y认为值A没有发生过变化。实质:只关心首尾相同,中间或许会有变化。A—>B—>…—>A
代码示例

public class T {
    static AtomicInteger atomicInteger = new AtomicInteger();
    public static void main(String[] args) {
        System.out.println("---ABA问题---");
        System.out.println("原始值:" + atomicInteger.get());
        //0-->1-->0
        new Thread(() -> {
            atomicInteger.compareAndSet(0, 1);
            System.out.println("步骤一:" + atomicInteger.get());
            try {
                //线程A休眠1秒,保证线程A按照0-->1-->0执行
                TimeUnit.SECONDS.sleep(1);
                atomicInteger.compareAndSet(1, 0);
                System.out.println("步骤二:" + atomicInteger.get());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "thread A").start();

        new Thread(() -> {
            //线程B休眠3秒,保证线程A完成ABA问题
            try {
                TimeUnit.SECONDS.sleep(3);
                atomicInteger.compareAndSet(0, 2);
                System.out.println("步骤三:" + atomicInteger.get());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "thread B").start();
        while (Thread.activeCount() > 2) {}
        System.out.println("最终结果:" + atomicInteger.get());
    }
}

输出结果:
在这里插入图片描述

通过对ABA问题的理解,结合代码以及输出,可以清楚的看到,中间步骤有变化,但是没有被感知到。

ABA问题解决方案

了解了ABA问题的原因,就比较好给出解决方案了,加上时间戳(版本号),使得每次修改,版本号都变化,那么线程Y在修改当前值的时候,比对版本号,就会发现,版本号不一致,那么自己的值就一定需要重新获取了,在给出解决方案之前,我们先来看看原子引用。

原子引用
public class T {
    public static void main(String[] args) {
        Student student1 = new Student("kevin", 31);
        Student student2 = new Student("james", 33);
        AtomicReference<Student> atomicReference = new AtomicReference<>();
        atomicReference.set(student1);
        System.out.println("原始值:" + atomicReference.get().toString());
        atomicReference.compareAndSet(student1, student2);
        System.out.println("结果值:" + atomicReference.get().toString());
    }
}

输出结果:
在这里插入图片描述

结果分析:通过原子引用类型,我们可以设置任意的原子类型,例子中的是Student类,实际需要的或许是People类或者是其他类。了解了原子引用后,我们再看ABA问题解决方案代码,就更容易理解了。

ABA问题解决方案代码示例

public class T {
    //初始值为0,版本号为1
    static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(0, 1);
    public static void main(String[] args) {
        System.out.println("---ABA问题解决---");
        System.out.println("原始值:" + atomicStampedReference.getReference());
        //0-->1-->0
        new Thread(() -> {
            atomicStampedReference.compareAndSet(0, 1, 1, 2);
            System.out.println("步骤一值:" + atomicStampedReference.getReference() + "版本号:" + atomicStampedReference.getStamp());
            try {
                //线程A休眠1秒,保证线程A按照0-->1-->0执行
                TimeUnit.SECONDS.sleep(1);
                atomicStampedReference.compareAndSet(1, 0, 2, 3);
                System.out.println("步骤二值:" + atomicStampedReference.getReference() + "版本号:" + atomicStampedReference.getStamp());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "thread A").start();

        new Thread(() -> {
            //线程B休眠3秒,保证线程A完成ABA问题
            try {
                TimeUnit.SECONDS.sleep(3);
                atomicStampedReference.compareAndSet(0, 1, 1, 2);
                System.out.println("步骤三值:" + atomicStampedReference.getReference() + "版本号:" + atomicStampedReference.getStamp());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "thread B").start();
        while (Thread.activeCount() > 2) { }
        System.out.println("最终结果值:" + atomicStampedReference.getReference());
    }

输出结果:
在这里插入图片描述

对比ABA问题的输出结果,可以看到,有了版本号后,最终的结果没有被修改成功,原因就在于版本号没有比对成功。代码中给出的原子时间戳引用的类型是Integer(AtomicStampedReference<Integer>),泛型的参数可以是任意的。

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值