17.AtomicInteger、AtomicBoolean的底层原理

2023年 10月 6日 68.7k 0

小陈:老王啊,今天就要开始Atomic原子类的学习了吧......

老王:是啊,之前我们只是简单介绍了Atomic的体系,今天我们就要进入Atomic底层原理的的学习了,首先我们从AtomicInteger这个比较简单的原子类开始,在说AtomicInteger的底层原理之前呢,我先给你看两个例子:

实测样例对比Integer和AtomicInteger的线程安全性

Integer的测试样例

(1)定义一个共享变量Integer

(2)定义一个新的线程类,创建两个线程,每个线程执行10000次value++ 操作

public class AddDemo {
    // 定义一个Integer类型的共享变量value
    private static Integer value = 0;
 
    public static class AddThread extends Thread{
        @Override
        public void run() {
           for (int i = 0; i < 10000; i++) {
               value++;
           }
        }
    }
 
    public static void main(String[] args) throws InterruptedException {
        // 定义两个线程各自对value执行10000次自增操作
        AddThread addThread1 = new AddThread();
        AddThread addThread2 = new AddThread();
 
        // 启动两个线程
        addThread1.start();
        addThread2.start();
 
        // 主线程等待两个线程执行完毕
        addThread1.join();
        addThread2.join();
 
        // 输出最新的value结果
        System.out.println("value的值为:" + value);
    }
}

看看最后得到的结果19513,比预期20000相差还是挺大的

image.png

AtomicInteger的测试样例

(1)定义一个AtomicInteger原子类

(2)定义一个新的线程类AtomicAddThread,创建两个线程,每个线程执行10000次incrementAndGet() 操作

public class AtomicAddDemo {
 
    private static AtomicInteger value = new AtomicInteger(0);
 
    public static class AtomicAddThread extends Thread{
        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                value.incrementAndGet();
            }
        }
    }
 
    public static void main(String[] args) throws InterruptedException {
        // 定义两个线程各自对value执行10000次自增操作
        AtomicAddThread atomicAddThread1 = new AtomicAddThread();
        AtomicAddThread atomicAddThread2 = new AtomicAddThread();
 
        // 启动两个线程
        atomicAddThread1.start();
        atomicAddThread2.start();
 
        // 主线程等待两个线程执行完毕
        atomicAddThread1.join();
        atomicAddThread2.join();
 
        // 输出最新的value结果
        System.out.println("value的值为:" + value.get());
    }
}

实际的结果20000,与预期的结果准确无误

image.png

老王:小陈啊,通过上述的实际例子,说明 AtomicInteger原子类确实是线程安全的。

小陈:是啊,使用AtomicInteger两个线程执行20000次自增操作得到的结果于预期值一致,那AtomicInteger底层到底是怎么确保线程安全的呢?

老王:这个啊,我们慢慢来剖析......

AtomicInteger的内部属性

老王:我们先通过源码来看一下AtomicInteger内部有哪些属性以及作用是什么:

public class AtomicInteger extends Number implements java.io.Serializable {
    // unsafe对象,可以直接根据内存地址操作数据,可以突破java语法的限制
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    // 存储实际的值
    private volatile int value;
    // 存储value属性在AtomicInteger类实例内部的偏移地址
    private static final long valueOffset;
    static {
        try {
            // 在类初始化的时候就获取到了value变量在对象内部的偏移地址
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }
}

(1)首先内部持有一个unsafe对象,Atomic原子类底层的操作都是基于unsafe对象来进行的

(2)然后有一个volatile int value变量,这个value就是原子类的实际数值,使用volatile来修饰,volatile可以保证并发中的可见性和有序性(这里之前讲过volatile可以保证可见性和有序性,不记得的要回去重新看一下哦)

(3)还有一个valueOffset,看看这段代码,其实就是获得value属性在AtomicInteger对象内部的偏移地址的:

 valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));

这个value属性相对于AtomicInter对象的内部偏移量存储在valueOffset中,我们之前讲过的 ,通过unsafe类是直接在内存级别去给变量赋值的。这里啊,我们再回顾一下unsafe值怎么从内存级别操作数据的:

  • 首先要知道你要操作对象的内存地址,也就是AtomicInteger对象引用指向的内存地址
    • 其次是要知道value属性在对象内部的偏移量offset,就可以通过 (对象地址 + offset偏移量) 直接找到value变量在内存的地址是多少,然后就可以直接给这块内存赋值了。

image.png

小陈:额,这个AtomicInteger内部还是蛮简单的呀,一个 volatile int value的属性、一个unsafe类、一个偏移地址就完事了

老王:哈哈,是啊,其实Atomic原子类啊,就是对基础的类型进行了一下包装而已,使得他们是线程安全的。比如AtomicInteger要对int进行包装,所以它内部肯定是有一个属性来存储int的值的。至于它其他两个属性valueOffset、unsafe是辅助实现并发安全的属性。

AtomicInteger的构造方法

老王:让我们再来看看AtomicInteger的构造方法源码:

public AtomicInteger(int initialValue) {
    value = initialValue;
}
 
public AtomicInteger() {
}

提供了两个构造方法,第一个是在创建AtomicInteger对象的时候直接给内存存储值的volatile int value设置初始化的值;第二个没有赋初始值,那默认就是0

AtomicInteger方法的源码分析

getAndIncrement()方法源码

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

我们看到AtomicInteger的getAndIncrement()方法源码很简单,底层就是基于unsafe.getAndAddInt包装了一下,让我们继续看一下unsafe.getAndAddInt方法源码:

public final int getAndAddInt(Object o, long valueOffset, int x) {
    int expected;
    do {
        expected = this.getIntVolatile(o, valueOffset);
    } while(!this.compareAndSwapInt(o, valueOffset, expected, expected + x));
    	
    return expected;
}

(1)首先 (o + valueOffset) 得到value变量在内存中的地址,然后根据地址直接取出value在主内存值,这个值记录为expected中

(2)根据  (o + offsetSet)地址偏移量,expected期待的值跟当前内存的值进行对比,如果相等则CAS操作成功,内存的值修改为 expected + x

(3)如果值不相等,则进入下一次循环,直到CAS操作成功为止。

(4)由于使用了volatile 修饰符修饰了value,所以一旦修改了别的线程能立马可见、同时volatile还是用内存屏障确保有序性

(5)所以上面的CAS操作确保了原子性,通过volatile确保可见性、有序性;线程安全的三个特性都满足了,上面的操作就是线程安全的。

小陈:原来这里AtomicInteger底层执行getAndIncrement() 操作底层就是直接调用unsafe的getAndAddInt()方法啊,最后还是走到了unsafe的compareAndSwapInt方法里面了,这里还是简单的呀。

老王:哈哈,AtomicInteger底层的源码本来就是不难的,底层都是基于unsafe进行薄薄的包装了一层而已,然后底层都是基于unsafe的CAS操作来保证原子性的,然后有使用volatile来修饰变量,保证了可见性和有序性,这样它就是线程安全的。

老王:关于unsafe的CAS操作是怎么保证原子性的,小陈你还记得住不,前两章的时候我们还画了一个图的:

image.png

小陈:嗯嗯,这个我记得的。

老王:好,那我也就不在CAS怎么保证原子性的话题上多说的了,我们继续看AtomicInteger原子类的其它源码:

AtomicInteger的compareAndSet源码:

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

底层也还是直接调用unsafe的compareAndSwapInt方法直接去修改,不过这里不同的是,只会执行一次CAS操作,即使失败了也不会重复CAS

其它方法源码:

其它的方法,基本都是直接调用unsafe.getAndInt方法,上面我们分析过了

public final int getAndDecrement() {
    return unsafe.getAndAddInt(this, valueOffset, -1);
}
 
public final int getAndAdd(int delta) {
    return unsafe.getAndAddInt(this, valueOffset, delta);
}

老王:好了,AtomicInteger的源码基本就分析到这里了,小陈关于AtomicInteger的底层原理这块,你还有其它的疑问不?

小陈:基本上没有了,AtomicInteger的底层还是比较简单的,基本都是调用unsafe的CAS操作确保原子性,然后使用volatile修饰变量,确保可见性和有序性,我理解上应该没问题了。

老王:好的,那我们就进入下一个原子类AtomicBoolean的讨论

AtomicBoolean 底层原理分析

AtomicBoolean 属性

 public class AtomicBoolean implements java.io.Serializable {
    // unsafe对象,可以直接根据内存地址操作数据,可以突破java语法的限制
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    // 存储实际的值
    private volatile int value;
    // 存储value属性在AtomicInteger类实例内部的偏移地址
    private static final long valueOffset;
    static {
        try {
            // 在类初始化的时候就获取到了value变量在对象内部的偏移地址
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }
}

小陈:啊这AtomicBoolean 拥有的属性怎么跟AtomicInteger是一模一样的!! ,它不是布尔类型吗?怎么使用一个int类型的 volatile int value来存储?

老王:其实啊,这只是AtomicBoolean 玩的一个小把戏,我们接着看就知道了:

我们看一下AtomicBoolean的构造函数源码:

public AtomicBoolean(boolean initialValue) {
    // 当传入initialValue为true的时候value = 1 , false的时候value = 0
    value = initialValue ? 1 : 0;
}

所以这里我们猜测,AtomicBoolean 底层就是使用一个int类型来表示true和false的,当value = 1的时候表示true,当value = 0的时候表示false。

然后继续看一下get()的源码:

直接就是判断value != 0 , 当value = 1则返回true,value = 0 返回false,证明了上面的猜想

public final boolean get() {
    return value != 0;
}

然后再看一下AtomicBoolean最常用最重要的方法compareAndSet源码:

public final boolean compareAndSet(boolean expect, boolean update) {
    int e = expect ? 1 : 0;
    int u = update ? 1 : 0;
    return unsafe.compareAndSwapInt(this, valueOffset, e, u);
}

底层就是将true 转成 1,将false转成 0,然后还是调用unsafe的compareAndSwapInt方法去执行CAS操作!!, 这个我们在上面将AtomicInteger的时候已经讲过了

小陈:哎呀,原来是这样啊,这个AtomicBoolean 耍花样啊,我还以为它底层使用布尔类型来存储值呢,哪知道这兄弟直接volatile 修饰的int类型,然后1 表示 true,0 表示false,这操作不都跟AtomicInteger一样吗?只是将value表示的意思换了一下而已......

老王:是啊,看过AtomicBoolean的底层源码之后恍然大悟了吧,很多功能啊其实实现起来没有那么难,还是有很多的方式的.....

小陈:恩恩,这个我认同......

老王:小陈啊,今天我们将AtomicInteger、AtomicBoolean 的底层原理就到这里了,我们明天继续......

小陈:我们下一章见。

目录

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中的所有评论

    发布评论