詳解Java高并發(fā)編程之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 改寫上面的代碼。
二、了解 AtomicReference2.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è)置。如下圖所示
volatile 有內(nèi)存屏障你知道嗎?
內(nèi)存屏障是啥啊?
內(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ā)詳解》這本書給出了我們一個(gè)答案
以上就是詳解Java高并發(fā)編程之AtomicReference的詳細(xì)內(nèi)容,更多關(guān)于Java高并發(fā)編程 AtomicReference的資料請(qǐng)關(guān)注好吧啦網(wǎng)其它相關(guān)文章!
相關(guān)文章:
1. 存儲(chǔ)于xml中需要的HTML轉(zhuǎn)義代碼2. XML入門的常見(jiàn)問(wèn)題(一)3. ASP實(shí)現(xiàn)加法驗(yàn)證碼4. ASP中if語(yǔ)句、select 、while循環(huán)的使用方法5. ASP.NET MVC使用異步Action的方法6. 匹配模式 - XSL教程 - 47. JS中map和parseInt的用法詳解8. SpringMVC+Jquery實(shí)現(xiàn)Ajax功能9. XML入門精解之結(jié)構(gòu)與語(yǔ)法10. CSS Hack大全-教你如何區(qū)分出IE6-IE10、FireFox、Chrome、Opera
