JUC Atomic原子类深入

什么是Atomic

Atomic是原子性的意思,可以自动更新,用于原子增量计数器之类的应用程序。可以解决多线程环境递增的异议性问题。

怎么使用Atomic

AtomicIntegerDemo

public class Atomic {
    AtomicInteger integer = new AtomicInteger(0);

    @Test
    public void testAtomicInteger() throws InterruptedException {
        ExecutorService executor = new ThreadPoolExecutor(10, 50, 20, TimeUnit.SECONDS, new LinkedBlockingQueue<>(10));
        for (int i = 0; i < 100; i++) {
            executor.execute(this::add);
            TimeUnit.MILLISECONDS.sleep(1);
        }
        executor.shutdown();
    }

    public void add() {
        for (int i = 0; i < 100; i++) {
            System.out.println(integer.incrementAndGet());
        }
    }
}
运行结果
AtomicInteger Demo运行结果

为什么要使用Atomic类

首先看一下不使用Atomic,以上Demo的运行结果会有什么问题

不使用AtomicDemo

public class IntDemo {
    int a = 1;

    @Test
    public void testInt() {
        final int[] int1 = {0};
        ExecutorService executor = new ThreadPoolExecutor(10, 50, 20, TimeUnit.SECONDS, new LinkedBlockingQueue<>(10));
        for (int i = 0; i < 10; i++) {
            executor.execute(() -> {
                for (int j = 0; j < 10; j++) {
                    int1[0]++;
                    System.out.println(int1[0]);
                }
            });
        }
        executor.shutdown();
    }
}
运行结果
int Demo运行结果

从运行结果来看,最终的结果不为100,可见在多线程的环境下,int自增操作并不是原子性的,这样就会导致一些问题。

为什么会出现这个问题?
1. 首先先了解一下,底层CPU、缓存已经主存之间的关系(intel)
CPU 缓存 主存
  • 主存(Main Memory):允许application,Java代码将会编译成字节码,然后由操作系统翻译成机器码,最后加载到内存中。
  • L3 unified cache:L3级缓存,这一块的数据的是被封装的CPU的所有核心共享的,也是三级缓存中容量最大。
  • L2 unified cache:L2级缓存,这一块的数据是被单个核心所独享的。
  • L1 unified cache:L2级缓存,这一块的数据是被单个核心所独享的。
  • ALU:CPU计算单元,负责数理逻辑计算。
  • Register:寄存器单元,其中包含若干的寄存器,有PC(程序计数器)、IR(指令寄存器)、DR(数据寄存器)等。

以上程序的共享变量i,没有进行锁、同步等数据一致性处理。变量i会被从内存读取到CPU封装的L3缓存,如果在多线程环境下,会存在两个操作变量i的线程同时跑在不同的核心上。

假设线程A->Core1,线程B->Core2,线程A从L3中读取到变量i为0,线程B从L3中读取到变量i也为0,线程A对变量i++得到i=1,同样线程B也对变量i++得到i=1,回写到内存时i=1,但是实际上已经进行了两次的++,故结果不正确。

特殊:四核八线程,对于一个核心,为了提高ALU的计算效率,会存在一个ALU单元对应两组Register,也就是所谓的超线程。此处的数据同步问题,博主还在学习。【如果有大佬了解,可以一起研究研究】

2. 再来了解一下,JMM(Java Memory Model)
JMM模型
  1. 线程A从主存中将变量读取到本地内存,仅仅是读取,后续还要进行加载。
  2. 将读取到的变量加载到本地工作内存,此时变量是主存中变量的副本。
  3. 将线程本地变量读取到执行引擎进行计算。
  4. 将第3步的计算结果刷回到线程本地工作内存中。
  5. 将本地工作内存写入到主存中,仅仅是写操作,后续还要进行存储。
  6. 将线程本地计算结果写回主存中。
  7. 线程B和线程B可以同时进行以上6步。

从以上的JMM模型的执行流程来看,当多线程的环境下,线程A和线程B可以同时读取主存中的变量,然后复制到本地工作内存中,接着计算,最后在将计算结果写回到主存中会存在数据不一致性。

Atomic原子类是如何保证并发环境数据一致性的?

上源码

在前文中,对于AtomicInteger递增是调用的incrementAndGet

incrementAndGet源码

从源码中可见,调用的是unsafe.getAndAddInt,让我们来看看这个方法的实现。

unsafe.getAndAddInt源码

从源码中可见,先是以getIntVolatile的方法(native方法)获取变量的值,然后调用compareAndSwapInt的方法(著名的CAS)进行数据的更改操作。

CAS原理(类似于一种乐观锁的概念)

CAS(compare and swap),比较并交换。


CAS原理
  1. 在并发环境下
  2. 读取:
    • 每个修改共享变量的线程都可以读取并进行修改
  3. 写入:
    • 如果此时的数据等于该线程一开始读取到的值,则将计算结果写入到主存中,
    • 否则就重新读取最新值,然后进行重新计算,反复如此操作,直到写入成功。
  4. 针对于其中的ABA问题,可以使用一个version来解决,version可以是uuid或者时间戳等,具体可以取决于业务场景。

深入源码(以AtomicInteger为例)

类关系图

类关系图

如图,可知AtomicInteger继承了Number抽象类,此抽象类中定义了一些关于数字之间的一些基础操作,具体方法如下图。

Number抽象类源码

成员变量

成员变量

构造方法&set&get

构造方法&set&get
  • 构造方法有两个:一个无参构造器、一个有参构造器
  • get:获取value
  • set:设置value
  • lazySet:异步设置value

加/减/设值操作

加/减/设值操作

加/减/设值操作
  • getAndSet(int newValue):将变量值设置成newValue,并返回旧值。
  • compareAndSet(int expect, int update):比较并设置值,只有当原有的value=expect时,才会将变量值设置成update,返回操作结果。
  • weakCompareAndSet:与compareAndSet(int expect, int update)类似,但是不强制原子性。
  • getAndIncrement():原子递增,返回旧值。
  • getAndDecrement():原子递减,返回旧值。
  • getAndAdd(int delta):原子增加delta,返回旧值。
  • incrementAndGet():原子递增,返回新值。
  • decrementAndGet():原子递减,返回新值。
  • addAndGet(int delta):原子增加delta,返回新值。

更新操作

getAndUpdate(IntUnaryOperator updateFunction)
getAndUpdate源码

此方法会先获取之前的值,然后将updateFunction函数作用于之前的读取出来的值,最后将变量设置成计算得到的结果。此方法返回操作之前的旧值。

updateAndGet(IntUnaryOperator updateFunction)
updateAndGet源码

此方法会先获取之前的值,然后将updateFunction函数作用于之前的读取出来的值,最后将变量设置成计算得到的结果。此方法返回操作之后的新值。

累加操作

getAndAccumulate(int x, IntBinaryOperator accumulatorFunction)
getAndAccumulate源码

此方法会先获取之前的值,然后将accumulatorFunction函数作用于之前的读取出来的值(其实就是将之前的旧值+x),最后将变量设置成计算得到的结果。此方法返回操作之前的旧值。

accumulateAndGet(int x, IntBinaryOperator accumulatorFunction)
getAndAccumulate源码

此方法会先获取之前的值,然后将accumulatorFunction函数作用于之前的读取出来的值(其实就是将之前的旧值+x),最后将变量设置成计算得到的结果。此方法返回操作之后的新值。

总结:对于get在方法名称前面的话,那么会返回操作之前的旧值。如果子啊方法名称后面,那么会返回操作之后的新值

下次 浅谈一下 JMM模型和MESI

如果对您有帮助,记得关注、点赞、收藏

你可能感兴趣的:(JUC Atomic原子类深入)