深刻分析CAS(樂觀鎖)

什麼是CAS

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

爲何說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();
    }
}
複製代碼

輸出結果bash

"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,重複上述步驟直到成功
app

下面是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會讓代碼可讀性變差,這句話看你們怎麼理解了。jvm

相關文章
相關標籤/搜索