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

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,905评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,140评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,791评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,483评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,476评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,516评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,905评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,560评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,778评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,557评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,635评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,338评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,925评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,898评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,142评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,818评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,347评论 2 342

推荐阅读更多精彩内容