欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

詳解Java高并發(fā)編程之AtomicReference

 更新時間:2021年06月22日 11:18:13   作者:程序員cxuan  
此篇文章主要介紹了AtomicReference的出現(xiàn)背景,AtomicReference的使用場景,以及介紹了AtomicReference的源碼,重點方法的源碼分析

一、AtomicReference 基本使用

我們這里再聊起老生常談的賬戶問題,通過個人銀行賬戶問題,來逐漸引入 AtomicReference 的使用,我們首先來看一下基本的個人賬戶類

public class BankCard {

    private final String accountName;
    private final int money;

    // 構(gòu)造函數(shù)初始化 accountName 和 money
    public BankCard(String accountName,int money){
        this.accountName = accountName;
        this.money = money;
    }
    // 不提供任何修改個人賬戶的 set 方法,只提供 get 方法
    public String getAccountName() {
        return accountName;
    }
    public int getMoney() {
        return money;
    }
    // 重寫 toString() 方法, 方便打印 BankCard
    @Override
    public String toString() {
        return "BankCard{" +
                "accountName='" + accountName + '\'' +
                ", money='" + money + '\'' +
                '}';
    }
}

個人賬戶類只包含兩個字段:accountName 和 money,這兩個字段代表賬戶名和賬戶金額,賬戶名和賬戶金額一旦設(shè)置后就不能再被修改。

現(xiàn)在假設(shè)有多個人分別向這個賬戶打款,每次存入一定數(shù)量的金額,那么理想狀態(tài)下每個人在每次打款后,該賬戶的金額都是在不斷增加的,下面我們就來驗證一下這個過程。

public class BankCardTest {

    private static volatile BankCard bankCard = new BankCard("cxuan",100);

    public static void main(String[] args) {

        for(int i = 0;i < 10;i++){
            new Thread(() -> {
                // 先讀取全局的引用
                final BankCard card = bankCard;
                // 構(gòu)造一個新的賬戶,存入一定數(shù)量的錢
                BankCard newCard = new BankCard(card.getAccountName(),card.getMoney() + 100);
                System.out.println(newCard);
                // 最后把新的賬戶的引用賦給原賬戶
                bankCard = newCard;
                try {
                    TimeUnit.MICROSECONDS.sleep(1000);
                }catch (Exception e){
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

在上面的代碼中,我們首先聲明了一個全局變量 BankCard,這個 BankCard 由 volatile進行修飾,目的就是在對其引用進行變化后對其他線程可見,在每個打款人都存入一定數(shù)量的款項后,輸出賬戶的金額變化,我們可以觀察一下這個輸出結(jié)果。

可以看到,我們預(yù)想最后的結(jié)果應(yīng)該是 1100 元,但是最后卻只存入了 900 元,那 200 元去哪了呢?我們可以斷定上面的代碼不是一個線程安全的操作。

問題出現(xiàn)在哪里?

雖然每次 volatile 都能保證每個賬戶的金額都是最新的,但是由于上面的步驟中出現(xiàn)了組合操作,即獲取賬戶引用更改賬戶引用,每個單獨的操作雖然都是原子性的,但是組合在一起就不是原子性的了。所以最后的結(jié)果會出現(xiàn)偏差。

我們可以用如下線程切換圖來表示一下這個過程的變化。

可以看到,最后的結(jié)果可能是因為在線程 t1 獲取最新賬戶變化后,線程切換到 t2,t2 也獲取了最新賬戶情況,然后再切換到 t1,t1 修改引用,線程切換到 t2,t2 修改引用,所以賬戶引用的值被修改了兩次。

那么該如何確保獲取引用和修改引用之間的線程安全性呢?

最簡單粗暴的方式就是直接使用 synchronized 關(guān)鍵字進行加鎖了。

1.1、使用 synchronized 保證線程安全性

使用 synchronized 可以保證共享數(shù)據(jù)的安全性,代碼如下

public class BankCardSyncTest {

    private static volatile BankCard bankCard = new BankCard("cxuan",100);

    public static void main(String[] args) {
        for(int i = 0;i < 10;i++){
            new Thread(() -> {
                synchronized (BankCardSyncTest.class) {
                    // 先讀取全局的引用
                    final BankCard card = bankCard;
                    // 構(gòu)造一個新的賬戶,存入一定數(shù)量的錢
                    BankCard newCard = new BankCard(card.getAccountName(), card.getMoney() + 100);
                    System.out.println(newCard);
                    // 最后把新的賬戶的引用賦給原賬戶
                    bankCard = newCard;
                    try {
                        TimeUnit.MICROSECONDS.sleep(1000);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
    }
}

相較于 BankCardTest ,BankCardSyncTest 增加了 synchronized 鎖,運行 BankCardSyncTest 后我們發(fā)現(xiàn)能夠得到正確的結(jié)果。

修改 BankCardSyncTest.class 為 bankCard 對象,我們發(fā)現(xiàn)同樣能夠確保線程安全性,這是因為在這段程序中,只有 bankCard 會進行變化,不會再有其他共享數(shù)據(jù)。

如果有其他共享數(shù)據(jù)的話,我們需要使用 BankCardSyncTest.clas 確保線程安全性。

除此之外,java.util.concurrent.atomic 包下的 AtomicReference 也可以保證線程安全性。

我們先來認識一下 AtomicReference ,然后再使用 AtomicReference 改寫上面的代碼。

二、了解 AtomicReference

2.1、使用 AtomicReference 保證線程安全性

下面我們改寫一下上面的那個示例

public class BankCardARTest {

    private static AtomicReference<BankCard> bankCardRef = new AtomicReference<>(new BankCard("cxuan",100));

    public static void main(String[] args) {

        for(int i = 0;i < 10;i++){
            new Thread(() -> {
                while (true){
                    // 使用 AtomicReference.get 獲取
                    final BankCard card = bankCardRef.get();
                    BankCard newCard = new BankCard(card.getAccountName(), card.getMoney() + 100);
                    // 使用 CAS 樂觀鎖進行非阻塞更新
                    if(bankCardRef.compareAndSet(card,newCard)){
                        System.out.println(newCard);
                    }
                    try {
                        TimeUnit.SECONDS.sleep(1);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
    }
}

在上面的示例代碼中,我們使用了 AtomicReference 封裝了 BankCard 的引用,然后使用 get() 方法獲得原子性的引用,接著使用 CAS 樂觀鎖進行非阻塞更新,更新的標(biāo)準(zhǔn)是如果使用 bankCardRef.get() 獲取的值等于內(nèi)存值的話,就會把銀行卡賬戶的資金 + 100,我們觀察一下輸出結(jié)果。

可以看到,有一些輸出是亂序執(zhí)行的,出現(xiàn)這個原因很簡單,有可能在輸出結(jié)果之前,進行線程切換,然后打印了后面線程的值,然后線程切換回來再進行輸出,但是可以看到,沒有出現(xiàn)銀行卡金額相同的情況。

2.2、AtomicReference 源碼解析

在了解上面這個例子之后,我們來看一下 AtomicReference 的使用方法

AtomicReference 和 AtomicInteger 非常相似,它們內(nèi)部都是用了下面三個屬性

Unsafesun.misc 包下面的類,AtomicReference 主要是依賴于 sun.misc.Unsafe 提供的一些 native 方法保證操作的原子性。

Unsafe 的 objectFieldOffset 方法可以獲取成員屬性在內(nèi)存中的地址相對于對象內(nèi)存地址的偏移量。這個偏移量也就是 valueOffset ,說得簡單點就是找到這個變量在內(nèi)存中的地址,便于后續(xù)通過內(nèi)存地址直接進行操作。

value 就是 AtomicReference 中的實際值,因為有 volatile ,這個值實際上就是內(nèi)存值。

不同之處就在于 AtomicInteger 是對整數(shù)的封裝,而 AtomicReference 則對應(yīng)普通的對象引用。也就是它可以保證你在修改對象引用時的線程安全性。

2.2.1、get and set

我們首先來看一下最簡單的 get 、set 方法:

get() : 獲取當(dāng)前 AtomicReference 的值

set() : 設(shè)置當(dāng)前 AtomicReference 的值

get() 可以原子性的讀取 AtomicReference 中的數(shù)據(jù),set() 可以原子性的設(shè)置當(dāng)前的值,因為 get() 和 set() 最終都是作用于 value 變量,而 value 是由 volatile 修飾的,所以 get 、set 相當(dāng)于都是對內(nèi)存進行讀取和設(shè)置。如下圖所示

2.2.2、lazySet 方法

volatile 有內(nèi)存屏障你知道嗎?

內(nèi)存屏障是啥???

內(nèi)存屏障,也稱內(nèi)存柵欄,內(nèi)存柵障,屏障指令等, 是一類同步屏障指令,是 CPU 或編譯器在對內(nèi)存隨機訪問的操作中的一個同步點,使得此點之前的所有讀寫操作都執(zhí)行后才可以開始執(zhí)行此點之后的操作。也是一個讓CPU 處理單元中的內(nèi)存狀態(tài)對其它處理單元可見的一項技術(shù)。

CPU 使用了很多優(yōu)化,使用緩存、指令重排等,其最終的目的都是為了性能,也就是說,當(dāng)一個程序執(zhí)行時,只要最終的結(jié)果是一樣的,指令是否被重排并不重要。所以指令的執(zhí)行時序并不是順序執(zhí)行的,而是亂序執(zhí)行的,這就會帶來很多問題,這也促使著內(nèi)存屏障的出現(xiàn)。

語義上,內(nèi)存屏障之前的所有寫操作都要寫入內(nèi)存;內(nèi)存屏障之后的讀操作都可以獲得同步屏障之前的寫操作的結(jié)果。因此,對于敏感的程序塊,寫操作之后、讀操作之前可以插入內(nèi)存屏障。

內(nèi)存屏障的開銷非常輕量級,但是再小也是有開銷的,LazySet 的作用正是如此,它會以普通變量的形式來讀寫變量。

也可以說是:懶得設(shè)置屏障了

2.2.3、getAndSet 方法

以原子方式設(shè)置為給定值并返回舊值。它的源碼如下

它會調(diào)用 unsafe 中的 getAndSetObject 方法,源碼如下

可以看到這個 getAndSet 方法涉及兩個 cpp 實現(xiàn)的方法,一個是 getObjectVolatile ,一個是 compareAndSwapObject 方法,他們用在 do...while 循環(huán)中,也就是說,每次都會先獲取最新對象引用的值,如果使用 CAS 成功交換兩個對象的話,就會直接返回 var5 的值,var5 此時應(yīng)該就是更新前的內(nèi)存值,也就是舊值。

2.2.4、compareAndSet 方法

這就是 AtomicReference 非常關(guān)鍵的 CAS 方法了,與 AtomicInteger 不同的是,AtomicReference 是調(diào)用的 compareAndSwapObject ,而 AtomicInteger 調(diào)用的是 compareAndSwapInt 方法。這兩個方法的實現(xiàn)如下

路徑在 hotspot/src/share/vm/prims/unsafe.cpp 中。

我們之前解析過 AtomicInteger 的源碼,所以我們接下來解析一下 AtomicReference 源碼。

因為對象存在于堆中,所以方法 index_oop_from_field_offset_long 應(yīng)該是獲取對象的內(nèi)存地址,然后使用 atomic_compare_exchange_oop 方法進行對象的 CAS 交換。

這段代碼會首先判斷是否使用了 UseCompressedOops,也就是指針壓縮。

這里簡單解釋一下指針壓縮的概念:JVM 最初的時候是 32 位的,但是隨著 64 位 JVM 的興起,也帶來一個問題,內(nèi)存占用空間更大了 ,但是 JVM 內(nèi)存最好不要超過 32 G,為了節(jié)省空間,在 JDK 1.6 的版本后,我們在 64位中的 JVM 中可以開啟指針壓縮(UseCompressedOops)來壓縮我們對象指針的大小,來幫助我們節(jié)省內(nèi)存空間,在 JDK 8來說,這個指令是默認開啟的。

如果不開啟指針壓縮的話,64 位 JVM 會采用 8 字節(jié)(64位)存儲真實內(nèi)存地址,比之前采用4字節(jié)(32位)壓縮存儲地址帶來的問題:

  • 增加了 GC 開銷:64 位對象引用需要占用更多的堆空間,留給其他數(shù)據(jù)的空間將會減少,從而加快了 GC 的發(fā)生,更頻繁的進行 GC。
  • 降低 CPU 緩存命中率:64 位對象引用增大了,CPU 能緩存的 oop 將會更少,從而降低了 CPU 緩存的效率。

由于 64 位存儲內(nèi)存地址會帶來這么多問題,程序員發(fā)明了指針壓縮技術(shù),可以讓我們既能夠使用之前 4 字節(jié)存儲指針地址,又能夠擴大內(nèi)存存儲。

可以看到,atomic_compare_exchange_oop 方法底層也是使用了 Atomic:cmpxchg 方法進行 CAS 交換,然后把舊值進行 decode 返回 (我這局限的 C++ 知識,只能解析到這里了,如果大家懂這段代碼一定告訴我,讓我請教一波)

2.2.5、weakCompareAndSet 方法

weakCompareAndSet: 非常認真看了好幾遍,發(fā)現(xiàn) JDK1.8 的這個方法和 compareAndSet 方法完全一摸一樣啊,坑我。。。

但是真的是這樣么?并不是,JDK 源碼很博大精深,才不會設(shè)計一個重復(fù)的方法,你想想 JDK 團隊也不是會犯這種低級團隊,但是原因是什么呢?

《Java 高并發(fā)詳解》這本書給出了我們一個答案

以上就是詳解Java高并發(fā)編程之AtomicReference的詳細內(nèi)容,更多關(guān)于Java高并發(fā)編程 AtomicReference的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • Java集合中的CopyOnWriteArrayList使用詳解

    Java集合中的CopyOnWriteArrayList使用詳解

    這篇文章主要介紹了Java集合中的CopyOnWriteArrayList使用詳解,CopyOnWriteArrayList是ArrayList的線程安全版本,從他的名字可以推測,CopyOnWriteArrayList是在有寫操作的時候會copy一份數(shù)據(jù),然后寫完再設(shè)置成新的數(shù)據(jù),需要的朋友可以參考下
    2023-12-12
  • spring scheduled單線程和多線程使用過程中的大坑

    spring scheduled單線程和多線程使用過程中的大坑

    本文主要介紹了spring scheduled單線程和多線程使用過程中的大坑,文中通過示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2022-01-01
  • idea新建文件后文件夾消失的問題及解決

    idea新建文件后文件夾消失的問題及解決

    這篇文章主要介紹了idea新建文件后文件夾消失的問題及解決,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教
    2024-08-08
  • Mybatis-Plus中and()和or()的使用與原理詳解

    Mybatis-Plus中and()和or()的使用與原理詳解

    最近發(fā)現(xiàn)MyBatisPlus還是挺好用的,下面這篇文章主要給大家介紹了關(guān)于Mybatis-Plus中and()和or()的使用與原理的相關(guān)資料,文中通過實例代碼介紹的非常詳細,需要的朋友可以參考下
    2022-09-09
  • java 中鎖的性能提高辦法

    java 中鎖的性能提高辦法

    這篇文章主要介紹了java 中鎖的性能提高辦法的相關(guān)資料,需要的朋友可以參考下
    2017-02-02
  • Netty4之如何實現(xiàn)HTTP請求、響應(yīng)

    Netty4之如何實現(xiàn)HTTP請求、響應(yīng)

    這篇文章主要介紹了Netty4之如何實現(xiàn)HTTP請求、響應(yīng)問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2023-04-04
  • 圖解Java?ReentrantLock的條件變量Condition機制

    圖解Java?ReentrantLock的條件變量Condition機制

    想必大家都使用過wait()和notify()這兩個方法把,他們主要用于多線程間的協(xié)同處理。而RenentrantLock也支持這樣條件變量的能力,而且相對于synchronized?更加強大,能夠支持多個條件變量,本文就來詳細說說
    2022-10-10
  • 如何利用SpringBoot搭建WebService服務(wù)接口

    如何利用SpringBoot搭建WebService服務(wù)接口

    之前項目經(jīng)理想要開發(fā)一個webservice的協(xié)議,給我一個星期的時間,后面用springboot開發(fā)了webservice,這篇文章主要給大家介紹了關(guān)于如何利用SpringBoot搭建WebService服務(wù)接口的相關(guān)資料,需要的朋友可以參考下
    2023-11-11
  • Java線程三種命名方法詳解

    Java線程三種命名方法詳解

    這篇文章主要介紹了Java線程三種命名方法詳解,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下
    2020-02-02
  • Spring FactoriesLoader機制實例詳解

    Spring FactoriesLoader機制實例詳解

    這篇文章主要介紹了Spring FactoriesLoader機制實例詳解,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下
    2020-03-03

最新評論