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問題。