1. 程式人生 > >CAS原理分析

CAS原理分析

一、鎖機制

常用的鎖機制有兩種:

1、悲觀鎖:假定會發生併發衝突,遮蔽一切可能違反資料完整性的操作。悲觀鎖的實現,往往依靠底層提供的鎖機制;悲觀鎖會導致其它所有需要鎖的執行緒掛起,等待持有鎖的執行緒釋放鎖。

2、樂觀鎖:假設不會發生併發衝突,每次不加鎖而是假設沒有衝突而去完成某項操作,只在提交操作時檢查是否違反資料完整性。如果因為衝突失敗就重試,直到成功為止。樂觀鎖大多是基於資料版本記錄機制實現。為資料增加一個版本標識,比如在基於資料庫表的版本解決方案中,一般是通過為資料庫表增加一個 “version” 欄位來實現。讀取出資料時,將此版本號一同讀出,之後更新時,對此版本號加一。此時,將提交資料的版本資料與資料庫表對應記錄的當前版本資訊進行比對,如果提交的資料版本號大於資料庫表當前版本號,則予以更新,否則認為是過期資料。 

樂觀鎖的缺點是不能解決髒讀的問題。

在實際生產環境裡邊,如果併發量不大且不允許髒讀,可以使用悲觀鎖解決併發問題;但如果系統的併發非常大的話,悲觀鎖定會帶來非常大的效能問題,所以我們就要選擇樂觀鎖定的方法.

鎖機制存在以下問題:

(1)在多執行緒競爭下,加鎖、釋放鎖會導致比較多的上下文切換和排程延時,引起效能問題。
(2)一個執行緒持有鎖會導致其它所有需要此鎖的執行緒掛起。
(3)如果一個優先順序高的執行緒等待一個優先順序低的執行緒釋放鎖會導致優先順序倒置,引起效能風險。


二、CAS 操作

JDK 5之前Java語言是靠synchronized關鍵字保證同步的,這是一種獨佔鎖,也是是悲觀鎖。java.util.concurrent(

J.U.C)種提供的atomic包中的類,使用的是樂觀鎖,用到的機制就是CAS,CAS(Compare and Swap)有3個運算元,記憶體值V,舊的預期值A,要修改的新值B。當且僅當預期值A和記憶體值V相同時,將記憶體值V修改為B,否則什麼都不做。

現代的CPU提供了特殊的指令,允許演算法執行讀-修改-寫操作,而無需害怕其他執行緒同時修改變數,因為如果其他執行緒修改變數,那麼CAS會檢測它(並失敗),演算法可以對該操作重新計算。而 compareAndSet() 就用這些代替了鎖定。

以AtomicInteger為例,研究在沒有鎖的情況下是如何做到資料正確性的。

public class AtomicInteger extends Number implements java.io.Serializable {
    
    private volatile int value;


    
    public final int get() {
        return value;
    }
	
	public final int getAndIncrement() {
        for (;;) {
            int current = get();
            int next = current + 1;
            if (compareAndSet(current, next))
                return current;
        }
    }
    
    public final boolean compareAndSet(int expect, int update) {
		return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }


欄位value需要藉助volatile原語,保證執行緒間的資料是可見的(共享的)。這樣在獲取變數的值的時候才能直接讀取。然後來看看++i是怎麼做到的。getAndIncrement採用了CAS操作,每次從記憶體中讀取資料然後將此資料和+1後的結果進行CAS操作,如果成功就返回結果,否則重試直到成功為止。而compareAndSet利用JNI來完成CPU指令的操作。

public final boolean compareAndSet(int expect, int update) {   
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
 }
整體的過程就是這樣子的,利用CPU的CAS指令,同時藉助JNI來完成Java的非阻塞演算法。其它原子操作都是利用類似的特性完成的。
而整個J.U.C都是建立在CAS之上的,因此對於synchronized阻塞演算法,J.U.C在效能上有了很大的提升。

CAS第一個問題是會導致“ABA問題”。
aba實際上是樂觀鎖無法解決髒資料讀取的一種體現。CAS演算法實現一個重要前提需要取出記憶體中某時刻的資料,而在下時刻比較並替換,那麼在這個時間差類會導致資料的變化。比如說一個執行緒one從記憶體位置V中取出A,這時候另一個執行緒two也從記憶體中取出A,並且two進行了一些操作變成了B,然後two又將V位置的資料變成A,這時候執行緒one進行CAS操作發現記憶體中仍然是A,然後one操作成功。儘管執行緒one的CAS操作成功,但是不代表這個過程就是沒有問題的。如果連結串列的頭在變化了兩次後恢復了原值,但是不代表連結串列就沒有變化。因此AtomicStampedReference/AtomicMarkableReference就很有用了。

AtomicMarkableReference 類描述的一個<Object,Boolean>的對,可以原子的修改Object或者Boolean的值,這種資料結構在一些快取或者狀態描述中比較有用。這種結構在單個或者同時修改Object/Boolean的時候能夠有效的提高吞吐量。 


AtomicStampedReference 類維護帶有整數“標誌”的物件引用,可以用原子方式對其進行更新。對比AtomicMarkableReference 類的<Object,Boolean>,AtomicStampedReference 維護的是一種類似<Object,int>的資料結構,其實就是對物件(引用)的一個併發計數(標記版本戳stamp)。但是與AtomicInteger 不同的是,此資料結構可以攜帶一個物件引用(Object),並且能夠對此物件和計數同時進行原子操作。