併發——詳細介紹CAS機制

1、前言

  今天花了點時間瞭解了一下JDK1.8ConcurrentHashMap的實現,發現它實現的主要思想就是依賴於CAS機制。CAS機制是併發中比較重要的一個概念,因此今天這篇博客就來詳細介紹一下CAS機制以及Java中對CAS的適用。java


2、正文

 2.1 樂觀鎖與悲觀鎖

  在講CAS以前,先來理解兩個概念,即樂觀鎖和悲觀鎖:併發

  • 樂觀鎖:在併發下對數據進行修改時保持樂觀的態度,認爲在本身修改數據的過程當中,其餘線程不會對同一個數據進行修改,因此不對數據加鎖,可是會在最終更新數據前,判斷一下這個數據有沒有被修改,若沒有被修改,纔將它更新爲本身修改的值;
  • 悲觀鎖:在併發下對數據進行修改時保持悲觀的態度,認爲在本身修改數據的過程當中,其餘線程也會對數據進行修改,因此在操做前會對數據加鎖,在操做完成後纔將鎖釋放,而在釋放鎖以前,其餘線程沒法操做數據;

  CAS其實就是樂觀鎖的一種實現方式,而悲觀鎖比較典型的就是Java中的synchronized。下面我就來詳細介紹一下CAS的相關概念。this


 2.2 什麼是CAS?

  CAS全稱compare and swap——比較並替換,它是併發條件下修改數據的一種機制,包含三個操做數:atom

  • 須要修改的數據的內存地址(V);
  • 對這個數據的舊預期值(A);
  • 須要將它修改成的值(B);

  CAS的操做步驟以下:線程

  1. 修改前記錄數據的內存地址V;
  2. 讀取數據的當前的值,記錄爲A;
  3. 修改數據的值變爲B;
  4. 查看地址V下的值是否仍然爲A,若爲A,則用B替換它;若地址V下的值不爲A,表示在本身修改的過程當中,其餘的線程對數據進行了修改,則不更新變量的值,而是從新從步驟2開始執行,這被稱爲自旋

  經過以上四個步驟對內存中的數據進行修改,就能夠保證數據修改的原子性。CAS是樂觀鎖的一種實現,因此這裏介紹的步驟和樂觀鎖的定義差很少,仍是很好理解的。code


 2.3 Java中CAS的使用

  Java中大量使用的CAS,好比,在java.util.concurrent.atomic包下有不少的原子類,如AtomicIntegerAtomicBoolean......這些類提供對intboolean等類型的原子操做,而底層就是經過CAS機制實現的。好比AtomicInteger類有一個實例方法,叫作incrementAndGet,這個方法就是將AtomicInteger對象記錄的值+1並返回,與i++相似。可是這是一個原子操做,不會像i++同樣,存在線程不一致問題,由於i++不是原子操做。好比以下代碼,最終必定可以保證num的值爲200對象

// 聲明一個AtomicInteger對象
AtomicInteger num = new AtomicInteger(0);
// 線程1
new Thread(()->{
    for (int i = 0; i < 100; i++) {
        // num++
        num.incrementAndGet();
    }
}).start();
// 線程2
new Thread(()->{
    for (int i = 0; i < 100; i++) {
        // num++
        num.incrementAndGet();
    }
}).start();

Thread.sleep(1000);
System.out.println(num);

  咱們看看incrementAndGet方法的源碼:內存

public final int incrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

  這裏使用了一個unsafe對象,而unsafe對象是什麼呢?咱們知道,Java並不能像C或C++同樣,直接操做內存,可是JVM爲咱們提供了一個後門,就是sun.misc.Unsafe類,這個類爲咱們實現了不少硬件級別的原子方法,固然,這些方法都是native方法,使用其餘語言實現,而不是Java方法。而上面的另一個變量valueOffset就是咱們須要修改的變量在內存中的偏移量。也許上面這個方法並不能讓你感受使用了CAS,那再看看下面這個方法:rem

public final boolean compareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

  compareAndSetAtomicInteger的另外一個方法,它的做用就是給定一個預期的舊值expect,以及須要更新爲的值update,若當前變量的值是expect,就將其修改成update,不然不修改(這不就是CAS的思想嗎)。而它底層調用了unsafe對象的compareAndSwapInt方法,從這個名字能夠看出,它的實現使用的就是CAScompareAndSwapInt的三個參數valueOffsetexpect以及update,恰好對應了CAS操做的三個操做數。get


 2.4 CAS機制的ABA問題

  CAS機制雖然簡單,可是也存在一些缺陷,其中比較典型的就是ABA問題。什麼是ABA問題,我簡單介紹一下:

  1. 假設有三個線程T1T2T3,它們都要對一個變量num的值進行修改,且使用的都是CAS機制進行同步,假設num的初始值爲100
  2. 線程T1首先讀取了num的值,將它記錄爲舊預期A1 = 100,而後它想要將num的值修改成80,記錄B2 = 80,在執行num = B2前,線程發生了切換,切換到線程T2
  3. 假設T2毫無阻礙地修改了num的值,將它從100修改成80,而後線程再度切換,T3開始執行;
  4. T3也是毫無阻礙地修改了num,將它從80從新修改成100,線程再次切換回T1
  5. T1從上次運行的斷點恢復,也就是準備用B1的值覆蓋num,可是因爲CAS機制,它須要先檢測num的值是否等於它記錄的預期值A1,而後它發現A1 = num = 100,認爲num沒有被修改過,因而用B1覆蓋了num

  上面這種狀況就是CASABA問題:一個變量被修改,可是又被改了回去,在CAS機制中,將沒法察覺這種錯誤的現象。在線程T1被中斷的過程當中,num的值被修改,按照CAS的原則,T1應該放棄對num的修改,從頭開始執行。有人可能想問,修改回去以後,不就和沒修改同樣嗎,有什麼影響呢?乍一看確實如此,可是咱們考慮實際的應用場景,就會發現有些狀況下會出現問題,舉個簡單的例子:

小明去銀行取款,它的信用卡中有100元,他想要取出20,可是因爲系統異常,系統發起了兩個取款線程,一個對應上面的線程T1,一個對應線程T2,發生了和上面T一、T2如出一轍的狀況。假設在T1中斷的過程當中,小明的媽媽正好給他匯款20元,將T2修改的數據又復原了,錢又變回了100,對應上面的線程T3,此時將發生什麼狀況?線程T1和T2都會進行取錢操做,將100變成80,實際上總共扣了40元。可是,正確的狀況應該是,T1檢測到錢被修改過,就放棄修改,這樣纔不會形成錯誤。

  對於ABA問題的解決方案也很是簡單,那就是再添加一個變量——版本號。每一個變量都加上一個版本號,在它被修改時,也同步修改版本號,而CAS操做在修改前記錄版本號,若在最後更新變量時,記錄的版本號與當前版本號一致,表示沒有被修改,可直接更新。


 2.5 CAS的優缺點以及適用場景

(1)優勢

  前面也提到過,CAS是一種樂觀鎖,其優勢就是不須要加鎖就能進行原子操做;

(2)缺點

  CAS的缺點主有兩點:

  • CAS機制只能用在對某一個變量進行原子操做,沒法用來保證多個變量或語句的原子性(synchronized能夠);
  • 假設在修改數據的過程當中常常與其餘線程修改衝突,將致使須要屢次的從新嘗試;

(3)適用場景

  由上面分析的優缺點能夠看出,CAS適用於併發衝突發生頻率較低的場合,而對於併發衝突較頻繁的場合,CAS因爲不斷重試,反倒會下降效率。


3、總結

  CAS是一種在併發下實現原子操做的機制,可是隻能用來保證一個變量的原子性,適用於併發衝突頻率較低的場合。


4、參考

  推薦兩篇描述CAS的博客,這兩篇博客經過漫畫對CAS進行了很是詳細的描述:

相關文章
相關標籤/搜索