爲何會有 AtomicReference ?

我把本身以往的文章彙總成爲了 Github ,歡迎各位大佬 star
https://github.com/crisxuan/bestJavaerjava

咱們以前瞭解過了 AtomicInteger、AtomicLong、AtomicBoolean 等原子性工具類,下面咱們繼續瞭解一下位於 java.util.concurrent.atomic 包下的工具類。git

關於 AtomicInteger、AtomicLong、AtomicBoolean 相關的內容請查閱程序員

一場 Atomic XXX 的魔幻之旅github

關於 AtomicReference 這種 JDK 工具類的瞭解的文章比較枯燥,並非表明着文章質量的降低,由於我想搞出一整套 bestJavaer 的全方位解析,那就勢必離不開對 JDK 工具類的瞭解。面試

記住:技術要作長線編程

AtomicReference 基本使用

咱們這裏再聊起老生常談的帳戶問題,經過我的銀行帳戶問題,來逐漸引入 AtomicReference 的使用,咱們首先來看一下基本的我的帳戶類緩存

public class BankCard {

    private final String accountName;
    private final int money;

    // 構造函數初始化 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,這兩個字段表明帳戶名和帳戶金額,帳戶名和帳戶金額一旦設置後就不能再被修改。安全

如今假設有多我的分別向這個帳戶打款,每次存入必定數量的金額,那麼理想狀態下每一個人在每次打款後,該帳戶的金額都是在不斷增長的,下面咱們就來驗證一下這個過程。微信

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;
                // 構造一個新的帳戶,存入必定數量的錢
                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進行修飾,目的就是在對其引用進行變化後對其餘線程可見,在每一個打款人都存入必定數量的款項後,輸出帳戶的金額變化,咱們能夠觀察一下這個輸出結果。網絡

能夠看到,咱們預想最後的結果應該是 1100 元,可是最後卻只存入了 900 元,那 200 元去哪了呢?咱們能夠判定上面的代碼不是一個線程安全的操做。

問題出如今哪裏?

雖然每次 volatile 都能保證每一個帳戶的金額都是最新的,可是因爲上面的步驟中出現了組合操做,即獲取帳戶引用更改帳戶引用,每一個單獨的操做雖然都是原子性的,可是組合在一塊兒就不是原子性的了。因此最後的結果會出現誤差。

咱們能夠用以下線程切換圖來表示一下這個過程的變化。

能夠看到,最後的結果多是由於在線程 t1 獲取最新帳戶變化後,線程切換到 t2,t2 也獲取了最新帳戶狀況,而後再切換到 t1,t1 修改引用,線程切換到 t2,t2 修改引用,因此帳戶引用的值被修改了兩次

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

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

使用 synchronized 保證線程安全性

使用 synchronized 能夠保證共享數據的安全性,代碼以下

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;
                    // 構造一個新的帳戶,存入必定數量的錢
                    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 後咱們發現可以獲得正確的結果。

修改 BankCardSyncTest.class 爲 bankCard 對象,咱們發現一樣可以確保線程安全性,這是由於在這段程序中,只有 bankCard 會進行變化,不會再有其餘共享數據。

若是有其餘共享數據的話,咱們須要使用 BankCardSyncTest.clas 確保線程安全性。

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

咱們先來認識一下 AtomicReference ,而後再使用 AtomicReference 改寫上面的代碼。

瞭解 AtomicReference

使用 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 樂觀鎖進行非阻塞更新,更新的標準是若是使用 bankCardRef.get() 獲取的值等於內存值的話,就會把銀行卡帳戶的資金 + 100,咱們觀察一下輸出結果。

能夠看到,有一些輸出是亂序執行的,出現這個緣由很簡單,有可能在輸出結果以前,進行線程切換,而後打印了後面線程的值,而後線程切換回來再進行輸出,可是能夠看到,沒有出現銀行卡金額相同的狀況。

AtomicReference 源碼解析

在瞭解上面這個例子以後,咱們來看一下 AtomicReference 的使用方法

AtomicReference 和 AtomicInteger 很是類似,它們內部都是用了下面三個屬性

wTiyJH.png

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

Unsafe 的 objectFieldOffset 方法能夠獲取成員屬性在內存中的地址相對於對象內存地址的偏移量。這個偏移量也就是 valueOffset ,說得簡單點就是找到這個變量在內存中的地址,便於後續經過內存地址直接進行操做。

value 就是 AtomicReference 中的實際值,由於有 volatile ,這個值實際上就是內存值。

不一樣之處就在於 AtomicInteger 是對整數的封裝,而 AtomicReference 則對應普通的對象引用。也就是它能夠保證你在修改對象引用時的線程安全性。

get and set

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

get() : 獲取當前 AtomicReference 的值

set() : 設置當前 AtomicReference 的值

get() 能夠原子性的讀取 AtomicReference 中的數據,set() 能夠原子性的設置當前的值,由於 get() 和 set() 最終都是做用於 value 變量,而 value 是由 volatile 修飾的,因此 get 、set 至關於都是對內存進行讀取和設置。以下圖所示

lazySet 方法

volatile 有內存屏障你知道嗎?

內存屏障是啥啊?

內存屏障,也稱內存柵欄,內存柵障,屏障指令等, 是一類同步屏障指令,是 CPU 或編譯器在對內存隨機訪問的操做中的一個同步點,使得此點以前的全部讀寫操做都執行後才能夠開始執行此點以後的操做。也是一個讓CPU 處理單元中的內存狀態對其它處理單元可見的一項技術。

CPU 使用了不少優化,使用緩存、指令重排等,其最終的目的都是爲了性能,也就是說,當一個程序執行時,只要最終的結果是同樣的,指令是否被重排並不重要。因此指令的執行時序並非順序執行的,而是亂序執行的,這就會帶來不少問題,這也促使着內存屏障的出現。

語義上,內存屏障以前的全部寫操做都要寫入內存;內存屏障以後的讀操做均可以得到同步屏障以前的寫操做的結果。所以,對於敏感的程序塊,寫操做以後、讀操做以前能夠插入內存屏障。

內存屏障的開銷很是輕量級,可是再小也是有開銷的,LazySet 的做用正是如此,它會以普通變量的形式來讀寫變量。

也能夠說是:懶得設置屏障了

getAndSet 方法

以原子方式設置爲給定值並返回舊值。它的源碼以下

它會調用 unsafe 中的 getAndSetObject 方法,源碼以下

能夠看到這個 getAndSet 方法涉及兩個 cpp 實現的方法,一個是 getObjectVolatile ,一個是 compareAndSwapObject 方法,他們用在 do...while 循環中,也就是說,每次都會先獲取最新對象引用的值,若是使用 CAS 成功交換兩個對象的話,就會直接返回 var5 的值,var5 此時應該就是更新前的內存值,也就是舊值。

compareAndSet 方法

這就是 AtomicReference 很是關鍵的 CAS 方法了,與 AtomicInteger 不一樣的是,AtomicReference 是調用的 compareAndSwapObject ,而 AtomicInteger 調用的是 compareAndSwapInt 方法。這兩個方法的實現以下

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

咱們以前解析過 AtomicInteger 的源碼,因此咱們接下來解析一下 AtomicReference 源碼。

由於對象存在於堆中,因此方法 index_oop_from_field_offset_long 應該是獲取對象的內存地址,而後使用 atomic_compare_exchange_oop 方法進行對象的 CAS 交換。

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

這裏簡單解釋一下指針壓縮的概念:JVM 最初的時候是 32 位的,可是隨着 64 位 JVM 的興起,也帶來一個問題,內存佔用空間更大了 ,可是 JVM 內存最好不要超過 32 G,爲了節省空間,在 JDK 1.6 的版本後,咱們在 64位中的 JVM 中能夠開啓指針壓縮(UseCompressedOops)來壓縮咱們對象指針的大小,來幫助咱們節省內存空間,在 JDK 8來講,這個指令是默認開啓的。

若是不開啓指針壓縮的話,64 位 JVM 會採用 8 字節(64位)存儲真實內存地址,比以前採用4字節(32位)壓縮存儲地址帶來的問題:

  1. 增長了 GC 開銷:64 位對象引用須要佔用更多的堆空間,留給其餘數據的空間將會減小,
    從而加快了 GC 的發生,更頻繁的進行 GC。
  2. 下降 CPU 緩存命中率:64 位對象引用增大了,CPU 能緩存的 oop 將會更少,從而下降了 CPU 緩存的效率。

因爲 64 位存儲內存地址會帶來這麼多問題,程序員發明了指針壓縮技術,可讓咱們既可以使用以前 4 字節存儲指針地址,又可以擴大內存存儲。

能夠看到,atomic_compare_exchange_oop 方法底層也是使用了 Atomic:cmpxchg 方法進行 CAS 交換,而後把舊值進行 decode 返回 (我這侷限的 C++ 知識,只能解析到這裏了,若是你們懂這段代碼必定告訴我,讓我請教一波)

weakCompareAndSet 方法

weakCompareAndSet: 很是認真看了好幾遍,發現 JDK1.8 的這個方法和 compareAndSet 方法徹底一摸同樣啊,坑我。。。

可是真的是這樣麼?並非,JDK 源碼很博大精深,纔不會設計一個重複的方法,你想一想 JDK 團隊也不是會犯這種低級團隊,可是緣由是什麼呢?

《Java 高併發詳解》這本書給出了咱們一個答案

總結

此篇文章主要介紹了 AtomicReference 的出現背景,AtomicReference 的使用場景,以及介紹了 AtomicReference 的源碼,重點方法的源碼分析。此篇 AtomicReference 的文章基本上涵蓋了網絡上全部關於 AtomicReference 的內容了,遺憾的是就是 cpp 源碼可能分析的不是很到位,這須要充足的 C/C++ 編程知識,若是有讀者朋友們有最新的研究成果,請及時告訴我。

另外,添加個人微信 becomecxuan,加入每日一題羣,天天一道面試題分享,更多內容請參見個人 Github,成爲最好的 bestJavaer,已經收錄此篇文章,詳情見原文連接

我本身肝了六本 PDF,微信搜索「程序員cxuan」關注公衆號後,在後臺回覆 cxuan ,領取所有 PDF,這些 PDF 以下

六本 PDF 連接

相關文章
相關標籤/搜索