詳解Java高并發(fā)編程之AtomicReference
一、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è)屬性
Unsafe
是 sun.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使用詳解,CopyOnWriteArrayList是ArrayList的線程安全版本,從他的名字可以推測(cè),CopyOnWriteArrayList是在有寫操作的時(shí)候會(huì)copy一份數(shù)據(jù),然后寫完再設(shè)置成新的數(shù)據(jù),需要的朋友可以參考下2023-12-12spring scheduled單線程和多線程使用過(guò)程中的大坑
本文主要介紹了spring scheduled單線程和多線程使用過(guò)程中的大坑,文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-01-01Mybatis-Plus中and()和or()的使用與原理詳解
最近發(fā)現(xiàn)MyBatisPlus還是挺好用的,下面這篇文章主要給大家介紹了關(guān)于Mybatis-Plus中and()和or()的使用與原理的相關(guān)資料,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-09-09Netty4之如何實(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ī)制
想必大家都使用過(guò)wait()和notify()這兩個(gè)方法把,他們主要用于多線程間的協(xié)同處理。而RenentrantLock也支持這樣條件變量的能力,而且相對(duì)于synchronized?更加強(qiáng)大,能夠支持多個(gè)條件變量,本文就來(lái)詳細(xì)說(shuō)說(shuō)2022-10-10如何利用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-11Spring FactoriesLoader機(jī)制實(shí)例詳解
這篇文章主要介紹了Spring FactoriesLoader機(jī)制實(shí)例詳解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-03-03