【死磕Java并發(fā)】—– 深入分析CAS

CAS,Compare And Swap,即比較并交換。Doug lea大神在同步組件中大量使用CAS技術(shù)鬼斧神工地實現(xiàn)了Java多線程的并發(fā)操作。整個AQS同步組件、Atomic原子類操作等等都是以CAS實現(xiàn)的,甚至ConcurrentHashMap在1.8的版本中也調(diào)整為了CAS+Synchronized??梢哉fCAS是整個JUC的基石。

小編從事在線教育多年,將自己的資料整合建了一個QQ群,對于有興趣一起交流學(xué)習(xí)java的可以加群:732976516,里面有大神會給予解答,也會有許多的資源可以供大家學(xué)習(xí)分享,歡迎大家前來一起學(xué)習(xí)進步!

CAS分析

在CAS中有三個參數(shù):內(nèi)存值V、舊的預(yù)期值A(chǔ)、要更新的值B,當(dāng)且僅當(dāng)內(nèi)存值V的值等于舊的預(yù)期值A(chǔ)時才會將內(nèi)存值V的值修改為B,否則什么都不干。其偽代碼如下:

if(this.value == A){

this.value = B

return true;

}else{

return false;

}

JUC下的atomic類都是通過CAS來實現(xiàn)的,下面就以AtomicInteger為例來闡述CAS的實現(xiàn)。如下:

Unsafe是CAS的核心類,Java無法直接訪問底層操作系統(tǒng),而是通過本地(native)方法來訪問。不過盡管如此,JVM還是開了一個后門:Unsafe,它提供了硬件級別的原子操作。

valueOffset為變量值在內(nèi)存中的偏移地址,unsafe就是通過偏移地址來得到數(shù)據(jù)的原值的。

value當(dāng)前值,使用volatile修飾,保證多線程環(huán)境下看見的是同一個。

我們就以AtomicInteger的addAndGet()方法來做說明,先看源代碼:

內(nèi)部調(diào)用unsafe的getAndAddInt方法,在getAndAddInt方法中主要是看compareAndSwapInt方法:

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

該方法為本地方法,有四個參數(shù),分別代表:對象、對象的地址、預(yù)期值、修改值(有位伙伴告訴我他面試的時候就問到這四個變量是啥意思…)。該方法的實現(xiàn)這里就不做詳細(xì)介紹了,有興趣的伙伴可以看看openjdk的源碼。

CAS可以保證一次的讀-改-寫操作是原子操作,在單處理器上該操作容易實現(xiàn),但是在多處理器上實現(xiàn)就有點兒復(fù)雜了。

CPU提供了兩種方法來實現(xiàn)多處理器的原子操作:總線加鎖或者緩存加鎖。

總線加鎖:總線加鎖就是就是使用處理器提供的一個LOCK#信號,當(dāng)一個處理器在總線上輸出此信號時,其他處理器的請求將被阻塞住,那么該處理器可以獨占使用共享內(nèi)存。但是這種處理方式顯得有點兒霸道,不厚道,他把CPU和內(nèi)存之間的通信鎖住了,在鎖定期間,其他處理器都不能其他內(nèi)存地址的數(shù)據(jù),其開銷有點兒大。所以就有了緩存加鎖。

緩存加鎖:其實針對于上面那種情況我們只需要保證在同一時刻對某個內(nèi)存地址的操作是原子性的即可。緩存加鎖就是緩存在內(nèi)存區(qū)域的數(shù)據(jù)如果在加鎖期間,當(dāng)它執(zhí)行鎖操作寫回內(nèi)存時,處理器不在輸出LOCK#信號,而是修改內(nèi)部的內(nèi)存地址,利用緩存一致性協(xié)議來保證原子性。緩存一致性機制可以保證同一個內(nèi)存區(qū)域的數(shù)據(jù)僅能被一個處理器修改,也就是說當(dāng)CPU1修改緩存行中的i時使用緩存鎖定,那么CPU2就不能同時緩存了i的緩存行。

CAS缺陷

CAS雖然高效地解決了原子操作,但是還是存在一些缺陷的,主要表現(xiàn)在三個方法:循環(huán)時間太長、只能保證一個共享變量原子操作、ABA問題。

循環(huán)時間太長

如果CAS一直不成功呢?這種情況絕對有可能發(fā)生,如果自旋CAS長時間地不成功,則會給CPU帶來非常大的開銷。在JUC中有些地方就限制了CAS自旋的次數(shù),例如BlockingQueue的SynchronousQueue。

只能保證一個共享變量原子操作

看了CAS的實現(xiàn)就知道這只能針對一個共享變量,如果是多個共享變量就只能使用鎖了,當(dāng)然如果你有辦法把多個變量整成一個變量,利用CAS也不錯。例如讀寫鎖中state的高地位

ABA問題

CAS需要檢查操作值有沒有發(fā)生改變,如果沒有發(fā)生改變則更新。但是存在這樣一種情況:如果一個值原來是A,變成了B,然后又變成了A,那么在CAS檢查的時候會發(fā)現(xiàn)沒有改變,但是實質(zhì)上它已經(jīng)發(fā)生了改變,這就是所謂的ABA問題。對于ABA問題其解決方案是加上版本號,即在每個變量都加上一個版本號,每次改變時加1,即A —> B —> A,變成1A —> 2B —> 3A。

用一個例子來闡述ABA問題所帶來的影響。

有如下鏈表

假如我們想要把B替換為A,也就是compareAndSet(this,A,B)。線程1執(zhí)行B替換A操作,線程2主要執(zhí)行如下動作,A 、B出棧,然后C、A入棧,最終該鏈表如下:

完成后線程1發(fā)現(xiàn)仍然是A,那么compareAndSet(this,A,B)成功,但是這時會存在一個問題就是B.next = null,compareAndSet(this,A,B)后,會導(dǎo)致C丟失,改棧僅有一個B元素,平白無故把C給丟失了。

CAS的ABA隱患問題,解決方案則是版本號,Java提供了AtomicStampedReference來解決。AtomicStampedReference通過包裝[E,Integer]的元組來對對象標(biāo)記版本戳stamp,從而避免ABA問題。對于上面的案例應(yīng)該線程1會失敗。

AtomicStampedReference的compareAndSet()方法定義如下:

compareAndSet有四個參數(shù),分別表示:預(yù)期引用、更新后的引用、預(yù)期標(biāo)志、更新后的標(biāo)志。源碼部門很好理解預(yù)期的引用 == 當(dāng)前引用,預(yù)期的標(biāo)識 == 當(dāng)前標(biāo)識,如果更新后的引用和標(biāo)志和當(dāng)前的引用和標(biāo)志相等則直接返回true,否則通過Pair生成一個新的pair對象與當(dāng)前pair CAS替換。Pair為AtomicStampedReference的內(nèi)部類,主要用于記錄引用和版本戳信息(標(biāo)識),定義如下:

Pair記錄著對象的引用和版本戳,版本戳為int型,保持自增。同時Pair是一個不可變對象,其所有屬性全部定義為final,對外提供一個of方法,該方法返回一個新建的Pari對象。pair對象定義為volatile,保證多線程環(huán)境下的可見性。在AtomicStampedReference中,大多方法都是通過調(diào)用Pair的of方法來產(chǎn)生一個新的Pair對象,然后賦值給變量pair。如set方法:

public void set(V newReference, int newStamp) {

Pair current = pair;

if (newReference != current.reference || newStamp != current.stamp)

this.pair = Pair.of(newReference, newStamp);

}

下面我們將通過一個例子可以可以看到AtomicStampedReference和AtomicInteger的區(qū)別。我們定義兩個線程,線程1負(fù)責(zé)將100 —> 110 —> 100,線程2執(zhí)行 100 —>120,看兩者之間的區(qū)別。

運行結(jié)果:

運行結(jié)果充分展示了AtomicInteger的ABA問題和AtomicStampedReference解決ABA問題。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容