深入理解CAS(樂觀鎖)
java使用CAS之前
在JDK5之前Java語言是靠synchronized關(guān)鍵字保證同步的,這會導(dǎo)致有鎖,鎖機制存在以下問題:
- 在多線程競爭下,加鎖、釋放鎖會導(dǎo)致比較多的上下文切換和調(diào)度延時,引起性能問題
- 一個線程持有鎖會導(dǎo)致其他所有需要此鎖的線程掛起
- 如果一個優(yōu)先級高的線程等待一個優(yōu)先級低的線程釋放鎖會導(dǎo)致優(yōu)先級倒置,引起性能風(fēng)險
volatile是不錯的機制,但是volatile不能保證原子性。因此對于同步最終還是要回到鎖機制上來。
獨占鎖是一個悲觀鎖,synchronized就是一種獨占鎖,會導(dǎo)致其他所有需要鎖的線程掛起,等待持有鎖的線程釋放鎖。而另一種更加有效的鎖就是樂觀鎖,CAS就是一種樂觀鎖
什么是CAS
在java語言之前,并發(fā)就已經(jīng)廣泛存在并在服務(wù)器領(lǐng)域得到了大量的應(yīng)用。所以硬件廠商老早就在芯片中加入了大量支持并發(fā)操作的原語(原語是由若干條指令組成的,用于完成一定功能的一個過程,具有不可分割性,即原語的執(zhí)行必須是連續(xù)的,在執(zhí)行過程中不允許被中斷),從而在硬件層面提升效率。在intel的CPU中,使用cmpxchg指令。
在java發(fā)展初期,java語言是不能夠利用硬件提供的這些便利來提升系統(tǒng)性能的。而隨著java不斷的發(fā)展,java本地方法(JNI)的出現(xiàn),為java程序越過jvm直接調(diào)用本地方法提供了一種便捷的方式,因而java在并發(fā)的手段上也多了起來。而在Doug Lea提供的concurrent包中,CAS理論是他實現(xiàn)整個java并發(fā)包的基石。
CAS操作包含三個操作數(shù)—— 內(nèi)存位置的值(V)、預(yù)期原值(A)和新值(B)。如果內(nèi)存位置的值與預(yù)期原值相匹配,那么處理器會自動將該位置更新為新值。否則,處理器不做任何操作。無論哪種情況,它都會在CAS指令之前返回該位置的值。CAS有效地說明了“我認(rèn)為位置V應(yīng)該包含值A(chǔ);如果包含該值,則將B放到這個位置;否則,不要更改該位置,只告訴我這個位置現(xiàn)在的值即可。”
CAS是一種有名的無鎖算法。無鎖編程,即不適用鎖的情況下實現(xiàn)多線程之間的變量同步,也就是在沒有現(xiàn)成被阻塞的情況下實現(xiàn)變量的同步。
總結(jié)如下:
- CAS(Compare And Swap)比較并替換,是線程并發(fā)運行時用到的一種技術(shù)
- CAS是原子操作,保證并發(fā)安全,而不能保證并發(fā)同步
- CAS是CPU的一個指令(需要JNI調(diào)用Native方法,才能調(diào)用CPU的指令)
- CAS是非阻塞的、輕量級的樂觀鎖
為什么說CAS是樂觀鎖
樂觀鎖,嚴(yán)格來說并不是鎖,通過原子性來保證數(shù)據(jù)的同步,比如說數(shù)據(jù)庫的樂觀鎖,通過版本控制來實現(xiàn),所以CAS不會保證線程同步。樂觀的認(rèn)為在數(shù)據(jù)更新期間沒有其他線程影響。
CAS原理
CAS(Compare And Swap)就是將內(nèi)存值更新為需要的值,但是有個條件,內(nèi)存值必須與期望值相同。舉個例子,內(nèi)存值V、期望值A(chǔ)、更新值B,當(dāng)V == A的時候?qū)更新為B。
CAS應(yīng)用
由于CAS是CPU指令,我們只能通過JNI與操作系統(tǒng)交互,關(guān)于CAS的方法都在sun.misc包下Unsafe的類里,java.util.concurrent.atomic包下的原子類等通過CAS來實現(xiàn)原子操作。
使用樂觀鎖還是悲觀鎖
從上面對兩種鎖的介紹,我們知道兩種鎖各有優(yōu)缺點,不可認(rèn)為一種好于另一種,像樂觀鎖適用于寫比較少的情況下(多讀場景),即沖突真的很少發(fā)生的時候,這樣可以省去了鎖的開銷,加大了系統(tǒng)的吞吐量。但如果是多寫的情況,一般會經(jīng)常發(fā)生沖突,這就會導(dǎo)致CAS算法會不斷的進行retry,這樣反倒是降低了性能,所以一般多寫的場景下用悲觀鎖就比較合適。
CAS指令和具體源代碼
原子類例如AtomicInteger里的方法都很簡單,我們看一下getAndIncrement方法:
//該方法功能是Interger類型加1
public final int getAndIncrement() {
//主要看這個getAndAddInt方法
return unsafe.getAndAddInt(this, valueOffset, 1);
}
//var1 是this指針
//var2 是地址偏移量
//var4 是自增的數(shù)值,是自增1還是自增N
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
//獲取內(nèi)存值,這是內(nèi)存值已經(jīng)是舊的,假設(shè)我們稱作期望值E
var5 = this.getIntVolatile(var1, var2);
//compareAndSwapInt方法是重點,
//var5是期望值,var5 + var4是要更新的值
//這個操作就是調(diào)用CAS的JNI,每個線程將自己內(nèi)存里的內(nèi)存值M
//與var5期望值E作比較,如果相同將內(nèi)存值M更新為var5 + var4,否則做自旋操作
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
解釋一下getAndAddInt方法的流程:
假設(shè)有一下情景:
- A、B兩個線程
- jvm主內(nèi)存的值1,A、B工作內(nèi)存的值為1(工作內(nèi)存會拷貝一份主內(nèi)存的值)
- 當(dāng)前期望值為1,做加1操作
- 此時var5 = 1,var4 = 1;
- A線程將var5與工作內(nèi)存值M比較,比較var5是否等于1
- 如果相同則將工作內(nèi)存值修改為var5 + var4 即修改為2并同步到駐村,此時this + valueOffset指針里,示例變量value的值就是2,結(jié)束循環(huán)
- 如果不相同,則是B線程修改了主內(nèi)存的值,說明B線程已經(jīng)先于A線程做了加1操作,A線程沒有更新成功需要繼續(xù)循環(huán),注意此時var5更新為新的內(nèi)存值,假設(shè)當(dāng)前的內(nèi)存值是2,那么此時var5 = 2,var + var4 = 3,重復(fù)上述步驟直到成功(自旋),成功之后,內(nèi)存地址中的值就改變?yōu)?
CAS優(yōu)缺點
- 優(yōu)點
非阻塞的輕量級的樂觀鎖,通過CPU指令實現(xiàn),在資源競爭不激烈的情況下性能高,相比synchronized重量鎖,synchronized會進行比較復(fù)雜的加鎖、解鎖和喚醒操作。
- 缺點
- ABA問題: 線程C、D;線程D將A修改為B后又修改為A,此時C線程以為A沒有改變過,java的原子類AtomicStampedReference,通過控制變量值的版本號來保證CAS的正確性。具體解決思路就是在變量前追加上版本號,每次變量更新的時候把版本號加一,那么A - B - A就會變成1A - 2B - 3A。
- 自旋時間過長,消耗CPU資源,如果資源競爭激烈,多線程自旋長時間消耗資源