CAS 無鎖式同步機制

計算機系統中,CPU 和內存之間是經過總線進行通訊的,當某個線程佔有 CPU 執行指令的時候,會盡量的將一些須要從內存中訪問的變量緩存在本身的高速緩存區中,而修改也不會當即映射到內存。java

而此時,其餘線程將看不到內存中該變量的任何改動,這就是咱們說的內存可見性問題。連續的文章中,咱們總共提出了兩種解決辦法。git

其一是使用關鍵字 volatile 修飾共享的全局變量,而 volatile 的實現原理大體分兩個步驟,任何對於該變量的修改操做都會由虛擬機追加一條指令立馬將該變量所在緩存區中的值回寫內存,接着將失效該變量在其餘 CPU 緩存區的引用。也就意味着,其餘 CPU 若是再想要使用該變量,緩存中是沒有的,進而逼迫去訪問內存拿最新的數據。github

其二是使用關鍵字 synchronized 並藉助對象內置鎖實現數據一致性,主要思路是,若是一個線程由於競爭某個鎖失敗而被阻塞了,那麼它就認爲別的線程正在工做,極可能會改了某些共享變量的數據,進而在得到鎖後第一時間從新刷內存中的數據,同時一個線程走出同步代碼塊以前會同步數據到內存。算法

其實咱們也不多會使用第二種方法來解決內存可見性問題,着實有點大材小用的感受,使用 volatile 關鍵字算是一個比較經常使用的方式。可是 volatile 是有特定的適用場景的,也具備它的侷限性,咱們一塊兒來看。數組

volatile 的侷限性

廢話很少說,先看一段代碼:緩存

public class MainTest {
    private static volatile int count;

    @Test
    public void testVolatile() throws InterruptedException {
        Thread1[] thread1s = new Thread1[100];
        for (int i = 0; i < 100; i++){
            thread1s[i] = new Thread1();
            thread1s[i].start();
        }

        for (int j = 0; j < 100; j++){
            thread1s[j].join();
        }
        System.out.println(count);
    }
    //每一個線程隨機自增 count
    private class Thread1 extends Thread{
        @Override
        public void run(){
            try {
                Thread.sleep((long) (Math.random() * 500));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            count++;
        }
    }
}

咱們將變量 count 使用 volatile 進行修飾,而後建立一百個線程並啓動,按照咱們以前的理解,變量 count 的值一旦被修改就能夠被其餘線程立馬看到,不會緩存在本身的工做內存。可是結果卻不是這樣。安全

屢次運行,結果不盡相同微信

94併發

96dom

98

....

其實緣由很簡單,咱們只說過 volatile 會在變量值被修改後回寫內存並失效其餘 CPU 緩存中該變量的引用迫使其餘線程從主存中從新去獲取該變量的值。

可是 count++ 這個操做並非原子操做,以前咱們說過這一點,這個操做會使得 CPU 作如下幾件事情:

  • 從 CPU 緩存讀出變量的值放入寄存器 A 中
  • 爲 count 加一併將值保存在另外一個寄存器 B 中
  • 將寄存器 B 中的數據寫到緩存並經過緩存鎖回寫內存

而若是第一步剛執行結束,或第二步剛執行結束,但沒有執行第三步的時候,其餘的某個線程更改了該變量的值並失效了當前 CPU 中緩存中該變量的引用,那麼第三步會因爲緩存失效而先去內存中讀一個值過來,而後用寄存器 B 中的值覆蓋緩存並刷到內存中。

這就意味着,在此以前其餘線程的修改被覆蓋,進而咱們得不到咱們預期的結果。結論就是,volatile 關鍵字具備可見性而不具備原子性。

原子類型變量

JDK1.5 之後由 Doug Lea 大神設計的 java.util.concurrent.atomic 包中包含了原子類型相關的全部類。

image

其中,

  • AtomicBoolean:對應的 Boolean 類型的原子類型
  • AtomicInteger:對應的 Integer 類型的原子類型
  • AtomicLong:相似
  • AtomicIntegerArray:對應的數組類型
  • AtomicLongArray:相似
  • AtomicReference:對應的引用類型的原子類型
  • AtomicIntegerFieldUpdater:字段更新類型

剩餘的幾個類的做用,咱們稍後再詳細介紹。

針對基本類型所對應的原子類型,咱們以 AtomicInteger 這個類爲例,看看它的源碼實現狀況。

AtomicInteger 相關實現

image

內部定義了一個 int 類型的變量 value,而且 value 修飾爲 volatile,表示 value 這個字段值的任何修改都對其餘線程當即可見。

而構造函數容許你傳入一個初始的 value 數值,不傳的話就會致使 value 的值爲零。

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

這個方法就是原子的「i++」操做,咱們跟進去看:

public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}

幾個參數簡單說一下,var1 是咱們的 AtomicInteger 實例引用,var2 是一個字段偏移量,經過它咱們能夠定位到其中的 value 字段。var4 這裏固定爲一。

代碼的邏輯也是簡單的,取出內部 value 字段的值並暫存在變量 value5 中,而後再次判斷,若是 value 字段的值依然等於 value5,那麼將原子操做式將 value 修改成 value4 + value5,本質上就是加一。

不然,說明在當前線程上次訪問後,又有其餘線程修改了這個 value 字段的值,因而咱們從新獲取這個字段的值,直到沒有人修改成止並自增它。

這個 compareAndSwapInt 方法咱們通常把它叫作『CAS』,底層有系統指令作支撐,是一個比較並修改的原子指令,若是值等於 A 則將它修改成 B,不然返回。

AtomicInteger 中的其他方法大體相似,都是依賴這個『CAS』方法實現的。

  • int getAndAdd(int delta):自增 delta 並獲取修改以前的值
  • int incrementAndGet():自增並獲取修改後的值
  • int decrementAndGet():自減並獲取修改後的值
  • int addAndGet(int delta):自增 delta 並獲取修改後的值

基於這一點,咱們重構上述的線程不安全的 demo:

//構建一個原子類型變量 aCount
private static volatile AtomicInteger aCount = new AtomicInteger(0);
@Test
public void testAtomic() throws InterruptedException {
    Thread2[] threads = new Thread2[100];
    for (int i = 0; i < 100; i++){
        threads[i] = new Thread2();
        threads[i].start();
    }
    for (int i = 0; i < 100; i++){
        threads[i].join();
    }
    System.out.println(aCount.get());
}

private class Thread2 extends Thread{
    @Override
    public void run(){
        try {
            Thread.sleep((long) (500 * Math.random()));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //原子自增
        aCount.getAndIncrement();
    }
}

修改後的代碼不管運行多少次,總會獲得結果 100 。有關 AtomicLong、AtomicReference 的相關內容大體相似,都是依賴咱們這個『CAS』方法,這裏再也不贅述。

FieldUpdater 是基於反射來原子修改變量的值,這裏很少說了,下面咱們看看『CAS』的一些問題。

CAS 的侷限性

ABA 問題

CAS 有一個典型問題就是「ABA 問題」,咱們知道 CAS 工做的基本原理是,先讀取目標變量的值,而後調用原子指令判斷該值是否等於咱們指望的值,若是等於就認爲沒有被別人改過,不然視做數據髒了,從新去讀變量的值。

可是問題是,若是變量 a 的值爲 100,咱們的 CAS 方法也讀到了 100,接着來了一個線程將這個變量改成 999,以後又來一個線程再改了一下,改爲 100 。而輪到咱們的主線程發現 a 的值依然是 100,它視做沒有人和它競爭修改 a 變量,因而修改 a 的值。

這種狀況,雖然 CAS 會更新成功,可是會存在潛在的問題,中途加入的線程的操做對於後一個線程根本是不可見的。而通常的解決辦法是爲每一次操做加上加時間戳,CAS 不只關注變量的原始值,還關注上一次修改時間。

循環時間長開銷大

咱們的 CAS 方法通常都定義在一個循環裏面,直到修改爲功纔會退出循環,若是在某些併發量較大的狀況下,變量的值始終被別的線程修改,本線程始終在循環裏作判斷比較舊值,效率低下。

因此說,CAS 適用於併發量不是很高的狀況下,效率遠遠高於鎖機制。

只能保證一個變量的原子操做

CAS 只能對一個變量進行原子性操做,而鎖機制則不一樣,得到鎖以後,就能夠對全部的共享變量進行修改而不會發生任何問題,由於別人沒有鎖不能修改這些共享變量。

總結一下,鎖實際上是一種悲觀的思想,「我認爲全部人都會和我來競爭某些資源的使用,因此我獲得資源以後把它鎖上,用完再釋放掉鎖」,而 CAS 則是一種樂觀的思想,「我覺得只有我一我的在使用這些資源,假若有人也在使用,那我再次嘗試便可」。

CAS 是之後的各類併發容器的實現基石,是一種樂觀的、非阻塞式的算法,將有助於提高咱們的併發性能。


文章中的全部代碼、圖片、文件都雲存儲在個人 GitHub 上:

(https://github.com/SingleYam/overview_java)

歡迎關注微信公衆號:OneJavaCoder,全部文章都將同步在公衆號上。

image

相關文章
相關標籤/搜索