亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

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

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

一、AtomicReference 基本使用

我們這里再聊起老生常談的賬戶問(wèn)題,通過(guò)個(gè)人銀行賬戶問(wèn)題,來(lái)逐漸引入 AtomicReference 的使用,我們首先來(lái)看一下基本的個(gè)人賬戶類

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;
    }
    // 不提供任何修改個(gè)人賬戶的 set 方法,只提供 get 方法
    public String getAccountName() {
        return accountName;
    }
    public int getMoney() {
        return money;
    }
    // 重寫 toString() 方法, 方便打印 BankCard
    @Override
    public String toString() {
        return "BankCard{" +
                "accountName='" + accountName + '\'' +
                ", money='" + money + '\'' +
                '}';
    }
}

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

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

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)造一個(gè)新的賬戶,存入一定數(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();
        }
    }
}

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

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

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

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

我們可以用如下線程切換圖來(lái)表示一下這個(gè)過(guò)程的變化。

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

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

最簡(jiǎn)單粗暴的方式就是直接使用 synchronized 關(guān)鍵字進(jì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)造一個(gè)新的賬戶,存入一定數(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 鎖,運(yùn)行 BankCardSyncTest 后我們發(fā)現(xiàn)能夠得到正確的結(jié)果。

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

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

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

我們先來(lái)認(rèn)識(shí)一下 AtomicReference ,然后再使用 AtomicReference 改寫上面的代碼。

二、了解 AtomicReference

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

下面我們改寫一下上面的那個(gè)示例

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 樂(lè)觀鎖進(jìn)行非阻塞更新
                    if(bankCardRef.compareAndSet(card,newCard)){
                        System.out.println(newCard);
                    }
                    try {
                        TimeUnit.SECONDS.sleep(1);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
    }
}

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

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

2.2、AtomicReference 源碼解析

在了解上面這個(gè)例子之后,我們來(lái)看一下 AtomicReference 的使用方法

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

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

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

value 就是 AtomicReference 中的實(shí)際值,因?yàn)橛?volatile ,這個(gè)值實(shí)際上就是內(nèi)存值。

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

2.2.1、get and set

我們首先來(lái)看一下最簡(jiǎn)單的 get 、set 方法:

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

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

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

2.2.2、lazySet 方法

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

內(nèi)存屏障是啥?。?/p>

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

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

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

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

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

2.2.3、getAndSet 方法

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

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

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

2.2.4、compareAndSet 方法

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

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

我們之前解析過(guò) AtomicInteger 的源碼,所以我們接下來(lái)解析一下 AtomicReference 源碼。

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

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

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

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

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

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

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

2.2.5、weakCompareAndSet 方法

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

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

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

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

相關(guān)文章

  • Java集合中的CopyOnWriteArrayList使用詳解

    Java集合中的CopyOnWriteArrayList使用詳解

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

    spring scheduled單線程和多線程使用過(guò)程中的大坑

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

    idea新建文件后文件夾消失的問(wèn)題及解決

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

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

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

    java 中鎖的性能提高辦法

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

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

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

    圖解Java?ReentrantLock的條件變量Condition機(jī)制

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

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

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

    Java線程三種命名方法詳解

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

    Spring FactoriesLoader機(jī)制實(shí)例詳解

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

最新評(píng)論