深入分析CAS(樂觀鎖)

zuckerbergJu2.0發表於2018-02-11

什麼是CAS

(1)CAS(compare and swap) 比較並替換,比較和替換是執行緒併發演算法時用到的一種技術
(2)CAS是原子操作,保證併發安全,而不是保證併發同步
(3)CAS是CPU的一個指令
(4)CAS是非阻塞的、輕量級的樂觀鎖

為什麼說CAS是樂觀鎖

樂觀鎖,嚴格來說並不是鎖,通過原子性來保證資料的同步,比如說資料庫的樂觀鎖,通過版本控制來實現等,所以CAS不會保證執行緒同步。樂觀的認為在資料更新期間沒有其他執行緒影響

CAS原理

CAS(compare and swap) 比較並替換,就是將記憶體值更新為需要的值,但是有個條件,記憶體值必須與期望值相同。舉個例子,期望值 E、記憶體值M、更新值U,當E == M的時候將M更新為U。

CAS應用

由於CAS是CPU指令,我們只能通過JNI與作業系統互動,關於CAS的方法都在sun.misc包下Unsafe的類裡 java.util.concurrent.atomic包下的原子類等通過CAS來實現原子操作。

CAS舉例

/**
 * Created by Dell on 2018/2/6.
 */
public class CasLock {
    private static final CountDownLatch latch = new CountDownLatch(5);
    private static AtomicInteger i = new AtomicInteger(0);
    private static int p = 0;

    public static void main(String[] args) throws InterruptedException {
        long time = System.currentTimeMillis();
        ExecutorService pool = Executors.newFixedThreadPool(5);
        for(int j = 0; j < 5; j++) {
            pool.execute(new Runnable() {
                public void run() {
                    for(int k = 0; k < 10000; k++) {
                        p++;                //不是原子操作
                        i.getAndIncrement();//呼叫原子類加1
                    }
                    latch.countDown();
                }
            });
        }
        latch.await();//保證所有子執行緒執行完成
        System.out.println(System.currentTimeMillis() - time);
        System.out.println("p=" + p);
        System.out.println("i=" + i);
        pool.shutdown();
    }
}
複製程式碼

輸出結果

"C:\Program Files\Java\jdk1.8.0_91\bin\java" ...
8
p=43204//結果不正確
i=50000

Process finished with exit code 0
複製程式碼

根據結果我們發現,由於多執行緒非同步進行p++操作,導致結果不正確。
為什麼p++的記過不正確呢?比如兩個執行緒讀到p的值為1,然後做加1操作,這時候p的值是2,而不是3 而變數i的結果卻是對的,這就要歸功於CAS,下面我們具體看一下原子類。

CAS指令和具體原始碼

原子類例如AtomicInteger裡的方法都很簡單,大家看一看都能懂,我們具體看下getAndIncrement方法。下面貼出程式碼:

//該方法功能是Interger型別加1
public final int getAndIncrement() {
		//主要看這個getAndAddInt方法
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }

//var1 是this指標
//var2 是地址偏移量
//var4 是自增的數值,是自增1還是自增N
public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
	        //獲取記憶體值,這是記憶體值已經是舊的,假設我們稱作期望值E
            var5 = this.getIntVolatile(var1, var2);
            //compareAndSwapInt方法是重點,
            //var5是期望值,var5 + var4是要更新的值
            //這個操作就是呼叫CAS的JNI,每個執行緒將自己記憶體裡的記憶體值M
            //與var5期望值E作比較,如果相同將記憶體值M更新為var5 + var4,否則做自旋操作
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }
複製程式碼

解釋一下getAndAddInt方法的流程
假設有以下情景: 1.A、B兩個執行緒
2.jvm主記憶體的值1,A、B工作記憶體的值為1(工作記憶體會拷貝一份主記憶體的值)
3.當前期望值為1,做加1操作
4.此時var5 = 1, var4 = 1,
(1)A執行緒將var5與工作記憶體值M比較,比較var5是否等於1
(2)如果相同則將工作記憶體值修改為var5+var4 既修改為2並同步到主記憶體,此時this指標裡,示例變數value的值就是2,結束迴圈
(3)如果不相同則其B執行緒修改了主記憶體的值,說明B執行緒已經先於A執行緒做了加1操作,A執行緒沒有更新成功需要繼續迴圈,注意此時var5更新為新的記憶體值,假設當前的記憶體值是2,那麼此時var5 = 2, var5 + var4 = 3,重複上述步驟直到成功

下面是compareAndSwapInt本地方法的原始碼,可以看到使用cmpxchg指令實現CAS,在效率上有不錯的表現。

UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
  UnsafeWrapper("Unsafe_CompareAndSwapInt");
  oop p = JNIHandles::resolve(obj);
  jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
  return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END
複製程式碼

CAS優缺點

  • 優點
    非阻塞的輕量級的樂觀鎖,通過CPU指令實現,在資源競爭不激烈的情況下效能高,相比synchronized重量鎖,synchronized會進行比較複雜的加鎖,解鎖和喚醒操作。
  • 缺點
    (1)ABA問題 執行緒C、D,執行緒D將A修改為B後又修改為A,此時C執行緒以為A沒有改變過,java的原子類AtomicStampedReference,通過控制變數值的版本來保證CAS的正確性。
    (2)自旋時間過長,消耗CPU資源, 如果資源競爭激烈,多執行緒自旋長時間消耗資源。

CAS總結

CAS不僅是樂觀鎖,是種思想,我們也可以在日常專案中通過類似CAS的操作保證資料安全,但並不是所有場合都適合,曾看過帖子說,能用synchronized就不要用CAS,除非遇到效能瓶頸,因為CAS會讓程式碼可讀性變差,這句話看大家怎麼理解了。

相關文章