CAS原理

轉自:http://leexuehan.github.io/2015/09/03/CAS-%E5%8E%9F%E7%90%86/

CAS

CAS有3個操作數,內存值V,舊的預期值A,要修改的新值B。當且僅當預期值A和內存值V相同時,將內存值V修改爲B,否則什麼都不做。

下面請先看一個簡單的計數器的例子:

class Counter {

    private int value;

    public synchronized int getValue() {
        return value;
    }

    public synchronized int increment() {
        return ++value;
    }

    public synchronized int decrement() {
        return --value;
    }
} 

其實像這樣的鎖機制,滿足基本的需求是沒有問題的了,但是有的時候我們的需求並非這麼簡單,我們需要更有效,更加靈活的機制,synchronized關鍵字是基於阻塞的鎖機制,也就是說當一個線程擁有鎖的時候,訪問同一資源的其它線程需要等待,直到該線程釋放鎖。

這裏會有些問題:

首先,如果被阻塞的線程優先級很高很重要怎麼辦?

其次,如果獲得鎖的線程一直不釋放鎖怎麼辦?(這種情況是非常糟糕的)。

還有一種情況,如果有大量的線程來競爭資源,那CPU將會花費大量的時間和資源來處理這些競爭(事實上CPU的主要工作並非這些),同時,還有可能出現一些例如死鎖之類的情況,

最後,其實鎖機制是一種比較粗糙,粒度比較大的機制,相對於像計數器這樣的需求有點兒過於笨重,因此,對於這種需求我們期待一種更合適、更高效的線程安全機制。

這就需要用到 CAS 算法。

與 synchronized 相比,CAS 算法是一種非阻塞算法。

拿一個 AtomicInteger 的一個簡單的方法來作爲例子:

private volatile int value;

public final int get() {
     return value;
}

public final int incrementAndGet() {
    for (;;) {
        int current = get();
        int next = current + 1;
        if (compareAndSet(current, next))
            return next;
    }
}

在這裏採用了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的非阻塞算法。其它原子操作都是利用類似的特性完成的。

其中

unsafe.compareAndSwapInt(this, valueOffset, expect, update);

類似:

if (this == expect) {
    this = update
    return true;

} else {
    return false;
}

那麼問題來了,怎麼保證 比較: this == expect,替換: this = update,這兩個操作的原子性呢?

因爲 compareAndSwapInt 是一個本地方法調用,具體源碼這裏就不貼了。

實現原理大致如下:

如果程序是在多處理器上運行,就爲cmpxchg指令(可以從字面意思大致推斷,這是一個比較並交換指令,是最關鍵的一條指令)加上lock前綴(lock cmpxchg)。反之,如果程序是在單處理器上運行,就省略lock前綴(單處理器自身會維護單處理器內的順序一致性,不需要lock前綴提供的內存屏障效果)。

可以看出來,在本地方法調用時,其實也是通過一種加鎖的方式實現的,只不過實現得更加底層而已。所以這種非阻塞算法,表面上看起來沒有加鎖,實際上,是被包含到了底層進行的。

那麼CPU的鎖到底是什麼樣子的呢?

CPU 的鎖

CPU的鎖一共分3種:

1.處理器自動保證基本內存操作的原子性

首先處理器會自動保證基本的內存操作的原子性。處理器保證從系統內存當中讀取或者寫入一個字節是原子的,意思是當一個處理器讀取一個字節時,其他處理器不能訪問這個字節的內存地址。奔騰6和最新的處理器能自動保證單處理器對同一個緩存行裏進行16/32/64位的操作是原子的,但是複雜的內存操作處理器不能自動保證其原子性,比如跨總線寬度,跨多個緩存行,跨頁表的訪問。但是處理器提供總線鎖定和緩存鎖定兩個機制來保證複雜內存操作的原子性。

2.使用總線鎖保證原子性

第一個機制是通過總線鎖保證原子性。如果多個處理器同時對共享變量進行讀改寫(i++就是經典的讀改寫操作)操作,那麼共享變量就會被多個處理器同時進行操作,這樣讀改寫操作就不是原子的,操作完之後共享變量的值會和期望的不一致,舉個例子:如果i=1,我們進行兩次i++操作,我們期望的結果是3,但是有可能結果是2。

原因是有可能多個處理器同時從各自的緩存中讀取變量i,分別進行加一操作,然後分別寫入系統內存當中。那麼想要保證讀改寫共享變量的操作是原子的,就必須保證CPU1讀改寫共享變量的時候,CPU2不能操作緩存了該共享變量內存地址的緩存。

處理器使用總線鎖就是來解決這個問題的。所謂總線鎖就是使用處理器提供的一個LOCK#信號,當一個處理器在總線上輸出此信號時,其他處理器的請求將被阻塞住,那麼該處理器可以獨佔使用共享內存。簡言之,就是一個共享內存的鎖。

3.通過緩存鎖定保證原子性。

在同一時刻我們只需保證對某個內存地址的操作是原子性即可,但總線鎖定把CPU和內存之間通信鎖住了,這使得鎖定期間,其他處理器不能操作其他內存地址的數據,所以總線鎖定的開銷比較大,最近的處理器在某些場合下使用緩存鎖定代替總線鎖定來進行優化。

頻繁使用的內存會緩存在處理器的L1,L2和L3高速緩存裏,那麼原子操作就可以直接在處理器內部緩存中進行,並不需要聲明總線鎖,在奔騰6和最近的處理器中可以使用“緩存鎖定”的方式來實現複雜的原子性。

所謂“緩存鎖定”就是如果緩存在處理器緩存行中內存區域在LOCK操作期間被鎖定,當它執行鎖操作回寫內存時,處理器不在總線上聲言LOCK#信號,而是修改內部的內存地址,並允許它的緩存一致性機制來保證操作的原子性,因爲緩存一致性機制會阻止同時修改被兩個以上處理器緩存的內存區域數據,當其他處理器回寫已被鎖定的緩存行的數據時會起緩存行無效,在上面的例子中,當CPU1修改緩存行中的i時使用緩存鎖定,那麼CPU2就不能同時緩存了i的緩存行。

但是有兩種情況下處理器不會使用緩存鎖定。

第一種情況是:當操作的數據不能被緩存在處理器內部,或操作的數據跨多個緩存行(cache line),則處理器會調用總線鎖定。

第二種情況是:有些處理器不支持緩存鎖定。對於Inter486和奔騰處理器,就算鎖定的內存區域在處理器的緩存行中也會調用總線鎖定。

以上兩個機制我們可以通過Inter處理器提供了很多LOCK前綴的指令來實現。比如位測試和修改指令BTS,BTR,BTC,交換指令XADD,CMPXCHG和其他一些操作數和邏輯指令,比如ADD(加),OR(或)等,被這些指令操作的內存區域就會加鎖,導致其他處理器不能同時訪問它。

CAS 的缺點

CAS雖然很高效的解決原子操作,但是CAS仍然存在三大問題:ABA問題,循環時間長開銷大和只能保證一個共享變量的原子操作。

1.ABA問題。

因爲CAS需要在操作值的時候檢查下值有沒有發生變化,如果沒有發生變化則更新,但是如果一個值原來是A,變成了B,又變成了A,那麼使用CAS進行檢查時會發現它的值沒有發生變化,但是實際上卻變化了。ABA問題的解決思路就是使用版本號。在變量前面追加上版本號,每次變量更新的時候把版本號加一,那麼A-B-A 就會變成1A-2B-3A。

2.循環時間長開銷大。

自旋CAS如果長時間不成功,會給CPU帶來非常大的執行開銷。如果JVM能支持處理器提供的pause指令那麼效率會有一定的提升,pause指令有兩個作用,第一它可以延遲流水線執行指令(de-pipeline),使CPU不會消耗過多的執行資源,延遲的時間取決於具體實現的版本,在一些處理器上延遲時間是零。第二它可以避免在退出循環的時候因內存順序衝突(memory order violation)而引起CPU流水線被清空(CPU pipeline flush),從而提高CPU的執行效率。

3.只能保證一個共享變量的原子操作。

當對一個共享變量執行操作時,我們可以使用循環CAS的方式來保證原子操作,但是對多個共享變量操作時,循環CAS就無法保證操作的原子性,這個時候就可以用鎖,或者有一個取巧的辦法,就是把多個共享變量合併成一個共享變量來操作。比如有兩個共享變量i=2,j=a,合併一下ij=2a,然後用CAS來操作ij。從Java1.5開始JDK提供了AtomicReference類來保證引用對象之間的原子性,你可以把多個變量放在一個對象裏來進行CAS操作。

從 CAS 談到 Concurrent包的實現

總體而言, CAS 機制和 volatile 變量的讀寫方式構成了整個 Concurrent 包實現的基石。

如果我們仔細分析concurrent包的源代碼實現,會發現一個通用化的實現模式:

  1. 聲明共享變量爲volatile;

  2. 使用CAS的原子條件更新來實現線程之間的同步;

  3. 配合以volatile的讀/寫和CAS所具有的volatile讀和寫的內存語義來實現線程之間的通信。

AQS,非阻塞數據結構和原子變量類(java.util.concurrent.atomic包中的類),這些concurrent包中的基礎類都是使用這種模式來實現的,而concurrent包中的高層類又是依賴於這些基礎類來實現的。從整體來看,concurrent包的實現示意圖如下:

這裏寫圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章