JUC Atomic原子類深入

什么是Atomic

Atomic是原子性的意思,可以自動更新,用于原子增量計數(shù)器之類的應用程序??梢越鉀Q多線程環(huán)境遞增的異議性問題。

怎么使用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,可見在多線程的環(huán)境下,int自增操作并不是原子性的,這樣就會導致一些問題。

為什么會出現(xiàn)這個問題?
1. 首先先了解一下,底層CPU、緩存已經主存之間的關系(intel)
CPU 緩存 主存
  • 主存(Main Memory):允許application,Java代碼將會編譯成字節(jié)碼,然后由操作系統(tǒng)翻譯成機器碼,最后加載到內存中。
  • L3 unified cache:L3級緩存,這一塊的數(shù)據(jù)的是被封裝的CPU的所有核心共享的,也是三級緩存中容量最大。
  • L2 unified cache:L2級緩存,這一塊的數(shù)據(jù)是被單個核心所獨享的。
  • L1 unified cache:L2級緩存,這一塊的數(shù)據(jù)是被單個核心所獨享的。
  • ALU:CPU計算單元,負責數(shù)理邏輯計算。
  • Register:寄存器單元,其中包含若干的寄存器,有PC(程序計數(shù)器)、IR(指令寄存器)、DR(數(shù)據(jù)寄存器)等。

以上程序的共享變量i,沒有進行鎖、同步等數(shù)據(jù)一致性處理。變量i會被從內存讀取到CPU封裝的L3緩存,如果在多線程環(huán)境下,會存在兩個操作變量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,也就是所謂的超線程。此處的數(shù)據(jù)同步問題,博主還在學習?!救绻写罄辛私猓梢砸黄鹧芯垦芯俊?/p>

2. 再來了解一下,JMM(Java Memory Model)
JMM模型
  1. 線程A從主存中將變量讀取到本地內存,僅僅是讀取,后續(xù)還要進行加載。
  2. 將讀取到的變量加載到本地工作內存,此時變量是主存中變量的副本。
  3. 將線程本地變量讀取到執(zhí)行引擎進行計算。
  4. 將第3步的計算結果刷回到線程本地工作內存中。
  5. 將本地工作內存寫入到主存中,僅僅是寫操作,后續(xù)還要進行存儲。
  6. 將線程本地計算結果寫回主存中。
  7. 線程B和線程B可以同時進行以上6步。

從以上的JMM模型的執(zhí)行流程來看,當多線程的環(huán)境下,線程A和線程B可以同時讀取主存中的變量,然后復制到本地工作內存中,接著計算,最后在將計算結果寫回到主存中會存在數(shù)據(jù)不一致性。

Atomic原子類是如何保證并發(fā)環(huán)境數(shù)據(jù)一致性的?

上源碼

在前文中,對于AtomicInteger遞增是調用的incrementAndGet

incrementAndGet源碼

從源碼中可見,調用的是unsafe.getAndAddInt,讓我們來看看這個方法的實現(xiàn)。

unsafe.getAndAddInt源碼

從源碼中可見,先是以getIntVolatile的方法(native方法)獲取變量的值,然后調用compareAndSwapInt的方法(著名的CAS)進行數(shù)據(jù)的更改操作。

CAS原理(類似于一種樂觀鎖的概念)

CAS(compare and swap),比較并交換。


CAS原理
  1. 在并發(fā)環(huán)境下
  2. 讀?。?
    • 每個修改共享變量的線程都可以讀取并進行修改
  3. 寫入:
    • 如果此時的數(shù)據(jù)等于該線程一開始讀取到的值,則將計算結果寫入到主存中,
    • 否則就重新讀取最新值,然后進行重新計算,反復如此操作,直到寫入成功。
  4. 針對于其中的ABA問題,可以使用一個version來解決,version可以是uuid或者時間戳等,具體可以取決于業(yè)務場景。

深入源碼(以AtomicInteger為例)

類關系圖

類關系圖

如圖,可知AtomicInteger繼承了Number抽象類,此抽象類中定義了一些關于數(shù)字之間的一些基礎操作,具體方法如下圖。

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函數(shù)作用于之前的讀取出來的值,最后將變量設置成計算得到的結果。此方法返回操作之前的舊值。

updateAndGet(IntUnaryOperator updateFunction)
updateAndGet源碼

此方法會先獲取之前的值,然后將updateFunction函數(shù)作用于之前的讀取出來的值,最后將變量設置成計算得到的結果。此方法返回操作之后的新值。

累加操作

getAndAccumulate(int x, IntBinaryOperator accumulatorFunction)
getAndAccumulate源碼

此方法會先獲取之前的值,然后將accumulatorFunction函數(shù)作用于之前的讀取出來的值(其實就是將之前的舊值+x),最后將變量設置成計算得到的結果。此方法返回操作之前的舊值。

accumulateAndGet(int x, IntBinaryOperator accumulatorFunction)
getAndAccumulate源碼

此方法會先獲取之前的值,然后將accumulatorFunction函數(shù)作用于之前的讀取出來的值(其實就是將之前的舊值+x),最后將變量設置成計算得到的結果。此方法返回操作之后的新值。

總結:對于get在方法名稱前面的話,那么會返回操作之前的舊值。如果子啊方法名稱后面,那么會返回操作之后的新值

下次 淺談一下 JMM模型和MESI

如果對您有幫助,記得關注、點贊、收藏

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容