av一区二区在线观看_亚洲男人的天堂网站_日韩亚洲视频_在线成人免费_欧美日韩精品免费观看视频_久草视

您的位置:首頁(yè)技術(shù)文章
文章詳情頁(yè)

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

瀏覽:101日期:2022-08-10 09:34:23
目錄一、AtomicReference 基本使用1.1、使用 synchronized 保證線程安全性二、了解 AtomicReference2.1、使用 AtomicReference 保證線程安全性2.2、AtomicReference 源碼解析2.2.1、get and set2.2.2、lazySet 方法2.2.3、getAndSet 方法2.2.4、compareAndSet 方法2.2.5、weakCompareAndSet 方法一、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é)果。

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

可以看到,我們預(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ò)程的變化。

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

可以看到,最后的結(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é)果。

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

可以看到,有一些輸出是亂序執(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è)屬性

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

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è)置。如下圖所示

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

2.2.2、lazySet 方法

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è)置為給定值并返回舊值。它的源碼如下

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

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

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

可以看到這個(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)如下

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

路徑在 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 交換。

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

這段代碼會(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

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

標(biāo)簽: Java
相關(guān)文章:
主站蜘蛛池模板: 色婷婷综合网 | 69热视频在线观看 | 免费精品视频 | 91国内精精品久久久久久婷婷 | 国产精品99久久久久久宅男 | 粉嫩国产精品一区二区在线观看 | 亚洲第一区久久 | 亚洲久草 | 国产精品久久久久国产a级 欧美日韩国产免费 | h在线免费观看 | 国产午夜影院 | 欧美精品一区在线 | 中文字幕日韩欧美一区二区三区 | 久久久久久久一区二区 | 亚洲黄色av | 五月婷婷色 | 污污免费网站 | 亚洲国产精品久久久久久 | 亚洲一区二区三区四区五区午夜 | 午夜影视 | 国产一区欧美 | 久久久精品在线 | 成人免费淫片aa视频免费 | 精品久久一区 | 欧美一级毛片免费观看 | 成人久久网 | 99精品亚洲国产精品久久不卡 | 草草视频在线免费观看 | 黄网站在线观看 | 天天操天天摸天天爽 | 亚洲 欧美 日韩在线 | 伊人二区 | 国产日产精品一区二区三区四区 | 国产精品福利一区二区三区 | 超级碰在线| 韩日一区二区 | 亚洲欧美另类在线观看 | 日日骚av | 午夜视频在线观看网站 | 欧美日韩综合一区 | 99精品一区二区三区 |