深入理解CAS(樂觀鎖)

深入理解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è)有一下情景:

  1. A、B兩個線程
  2. jvm主內(nèi)存的值1,A、B工作內(nèi)存的值為1(工作內(nèi)存會拷貝一份主內(nèi)存的值)
  3. 當(dāng)前期望值為1,做加1操作
  4. 此時var5 = 1,var4 = 1;
    1. A線程將var5與工作內(nèi)存值M比較,比較var5是否等于1
    2. 如果相同則將工作內(nèi)存值修改為var5 + var4 即修改為2并同步到駐村,此時this + valueOffset指針里,示例變量value的值就是2,結(jié)束循環(huán)
    3. 如果不相同,則是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ù)雜的加鎖、解鎖和喚醒操作。

  • 缺點
  1. ABA問題: 線程C、D;線程D將A修改為B后又修改為A,此時C線程以為A沒有改變過,java的原子類AtomicStampedReference,通過控制變量值的版本號來保證CAS的正確性。具體解決思路就是在變量前追加上版本號,每次變量更新的時候把版本號加一,那么A - B - A就會變成1A - 2B - 3A。
  1. 自旋時間過長,消耗CPU資源,如果資源競爭激烈,多線程自旋長時間消耗資源
最后編輯于
?著作權(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)容