為什么要有 AtomicReference ?
我們之前了解過(guò)了 AtomicInteger、AtomicLong、AtomicBoolean 等原子性工具類(lèi),下面我們繼續(xù)了解一下位于 java.util.concurrent.atomic 包下的工具類(lèi)。
關(guān)于 AtomicInteger、AtomicLong、AtomicBoolean 相關(guān)的內(nèi)容請(qǐng)查閱
一場(chǎng) Atomic XXX 的魔幻之旅
關(guān)于 AtomicReference 這種 JDK 工具類(lèi)的了解的文章比較枯燥,并不是代表著文章質(zhì)量的下降,因?yàn)槲蚁敫愠鲆徽?bestJavaer 的全方位解析,那就勢(shì)必離不開(kāi)對(duì) JDK 工具類(lèi)的了解。
記?。杭夹g(shù)要做長(zhǎng)線。
AtomicReference 基本使用
我們這里再聊起老生常談的賬戶問(wèn)題,通過(guò)個(gè)人銀行賬戶問(wèn)題,來(lái)逐漸引入 AtomicReference 的使用,我們首先來(lái)看一下基本的個(gè)人賬戶類(lèi)
- 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;
- }
- // 重寫(xiě) toString() 方法, 方便打印 BankCard
- @Override
- public String toString() {
- return "BankCard{" +
- "accountName='" + accountName + '\'' +
- ", money='" + money + '\'' +
- '}';
- }
- }
個(gè)人賬戶類(lèi)只包含兩個(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ù)量的錢(qián)
- 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)行加鎖了。
使用 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ù)量的錢(qián)
- 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 改寫(xiě)上面的代碼。
了解 AtomicReference
使用 AtomicReference 保證線程安全性
下面我們改寫(xiě)一下上面的那個(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)銀行卡金額相同的情況。
AtomicReference 源碼解析
在了解上面這個(gè)例子之后,我們來(lái)看一下 AtomicReference 的使用方法
AtomicReference 和 AtomicInteger 非常相似,它們內(nèi)部都是用了下面三個(gè)屬性
Unsafe 是 sun.misc 包下面的類(lèi),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í)的線程安全性。
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è)置。
lazySet 方法
volatile 有內(nèi)存屏障你知道嗎?
內(nèi)存屏障是啥啊?
內(nèi)存屏障,也稱內(nèi)存柵欄,內(nèi)存柵障,屏障指令等, 是一類(lèi)同步屏障指令,是 CPU 或編譯器在對(duì)內(nèi)存隨機(jī)訪問(wèn)的操作中的一個(gè)同步點(diǎn),使得此點(diǎn)之前的所有讀寫(xiě)操作都執(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)存屏障之前的所有寫(xiě)操作都要寫(xiě)入內(nèi)存;內(nèi)存屏障之后的讀操作都可以獲得同步屏障之前的寫(xiě)操作的結(jié)果。因此,對(duì)于敏感的程序塊,寫(xiě)操作之后、讀操作之前可以插入內(nèi)存屏障。
內(nèi)存屏障的開(kāi)銷(xiāo)非常輕量級(jí),但是再小也是有開(kāi)銷(xiāo)的,LazySet 的作用正是如此,它會(huì)以普通變量的形式來(lái)讀寫(xiě)變量。
也可以說(shuō)是:懶得設(shè)置屏障了
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)存值,也就是舊值。
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)銷(xiāo):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)教一波)
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è)答案。
總結(jié)
此篇文章主要介紹了 AtomicReference 的出現(xiàn)背景,AtomicReference 的使用場(chǎng)景,以及介紹了 AtomicReference 的源碼,重點(diǎn)方法的源碼分析。此篇 AtomicReference 的文章基本上涵蓋了網(wǎng)絡(luò)上所有關(guān)于 AtomicReference 的內(nèi)容了,遺憾的是就是 cpp 源碼可能分析的不是很到位,這需要充足的 C/C++ 編程知識(shí),如果有讀者朋友們有最新的研究成果,請(qǐng)及時(shí)告訴我。
本文轉(zhuǎn)載自微信公眾號(hào)「Java建設(shè)者」,可以通過(guò)以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系Java建設(shè)者公眾號(hào)。