1. 程式人生 > >CAS演算法實現資料更新的簡單理解和ABA問題

CAS演算法實現資料更新的簡單理解和ABA問題

注:本文是在http://www.toutiao.com/i6421671637946466817/?wxshare_count=2&pbid=1498719626基礎上整理!CAS:Compare and Swap,即比較再交換。是無鎖操作,也有人說他是樂觀鎖,也許是相對於獨佔鎖來說,原子性操作(通過作業系統指令來實現)重入鎖:java.util.concurrent.ReentrantLock和AutomicInteger就是使用CAS演算法實現的。它是通過演算法的來實現資料操作的互斥性CAS有3個運算元:記憶體值V(可能被thread修改)、預期值A(thread操作前看到記憶體時的值
)、要修改的新值B(當前thread操作後的值)。當且僅當預期值A和記憶體值V相同時,將記憶體值V修改為B,否則什麼都不做。該操作是一個原子操作,被廣泛的應用在Java的底層實現中。在Java中,CAS主要是由sun.misc.Unsafe這個類通過JNI呼叫CPU底層指令實現。舉例: 

thread1,thread2執行緒是同時更新同一變數9的值

因為thread1thread2執行緒都同時去訪問同一變數9,所以這兩個執行緒會把主記憶體的值完全拷貝一份到自己的工作記憶體空間,所以thread1thread2執行緒的記憶體預期值都為9。假設thread1在與thread2執行緒競爭中,執行緒thread1能去更新變數的值,而其他執行緒都失敗。

ps:失敗的執行緒並不會被掛起,而是被告知這次競爭中失敗,並可以再次發起嘗試thread1執行緒去更新變數值改為B:10,然後比較A:預期值9和記憶體的值C:9發現還沒被改變,然後寫到記憶體中。此時對於thread2來說,預期值A:9,記憶體值變為了C:10,A≠C,就操作失敗了.通俗解釋:CPU去更新一個值,但如果想改的值不再是原來的值,操作就失敗,因為很明顯,有其它操作先改變了這個值。

Linux GCC 支援的 CAS

GCC4.1+版本號中支援CAS的原子操作(完整的原子操作可參看 GCC Atomic Builtins

   1: bool __sync_bool_compare_and_swap (type *ptr, type oldval type newval, ...)
   2: type __sync_val_compare_and_swap (type *ptr, type oldval type newval, ...)

Windows支援的CAS

在Windows下。你能夠使用以下的Windows API來完畢CAS:(完整的Windows原子操作可參看MSDN的InterLocked Functions

   1: InterlockedCompareExchange ( __inout LONG volatile *Target,
   2:                                 __in LONG Exchange,
   3:                                 __in LONG Comperand);

C++ 11支援的CAS

C++11中的STL中的atomic類的函式能夠讓你跨平臺。(完整的C++11的原子操作可參看 Atomic Operation Library

   1: template< class T >
   2: bool atomic_compare_exchange_weak( std::atomic<T>* obj,
   3:                                    T* expected, T desired );
   4: template< class T >
   5: bool atomic_compare_exchange_weak( volatile std::atomic<T>* obj,
   6:                                    T* expected, T desired );

ABA問題:

雖然CAS操作是原子性操作--比較並交換。不過它的實現過程是首先讀取出記憶體中的值,我們稱之為“讀”,然後再進行運算,然後才是CAS指令操作,我們稱之為“寫”,【讀+寫】這個過程並不是原子性性的,當thread1和thread2同時讀取到記憶體值v=1後,thread1進行+1操作,更新後的值B=2,預期值A=1,然後比較預期值A是否跟此時的記憶體值相同,相同則操作成功,否則重試。java併發包中的java.util.concurrent.atomic.AtomicInteger類就用到了CAS操作:

    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;
        }
    }


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

this代表當前物件AutomicInteger,valueOffset代表value的記憶體地址,expect代表cas前讀取到的value值,update表示操作後的更新值。通過compareAndSet方法進行底層的CAS原子操作,如果:
valueOffset地址的值跟expect的值相等,則說明未被更新,則返回true。否則在死迴圈中重試直到成功。
繼續分析上邊的例子,thread1在進行cas前的這段時間內,也許會發生一些事情:thread2將進行了兩次CAS操作,先+1,再-1.

thread1在cas時發現記憶體值還是1,於是進行+1操作,記憶體值變為2,但是其實thread1後來讀取到的1的現場已經發生了變化

CAS開銷:CAS特點:速度快。原因:1CAS(比較並交換)是CPU指令級的操作,只有一步原子操作,所以非常快。2且CAS避免了請求作業系統來裁定鎖的問題,不用麻煩作業系統,直接在CPU內部就搞定了

CAS的開銷在於Cache miss:也就是對某一個變數執行 CAS 操作的 CPU 並不是最後一個操作該變數的那個CPU,而是系統中的其他CPU(假如8核CPU),所以對應的快取線並不存在於當前對變數執行CAS操作的那個 CPU 的快取記憶體中。所以需要將當前變數讀取到當前操作CPU的暫存器(快取記憶體中)。

首先介紹CPU的體系結構:

上圖可以看到一個8核CPU計算機系統,每個CPU有cache(CPU內部的快取記憶體,暫存器),管芯內還帶有一個互聯模組(Interconnect),使管芯內的兩個核可以互相通訊。在圖中央的系統互聯模組(SystemInterconnect)可以讓四個管芯相互通訊,並且將管芯與主存(Memory)連線起來。

資料以“快取線”為單位在系統中傳輸,“快取線”對應於記憶體中一個 2 的冪大小的位元組塊,大小通常為 32 到 256 位元組之間。

當 CPU 從記憶體中讀取一個變數到它的暫存器中時,必須首先將包含了該變數的快取線讀取到 CPU 快取記憶體。

同樣地,CPU 將暫存器中的一個值儲存到記憶體時,不僅必須將包含了該值的快取線讀到 CPU 快取記憶體,還必須確保沒有其他 CPU 擁有該快取線的拷貝。

比如,如果 CPU0 在對一個變數執行“比較並交換”(CAS)操作,而該變數所在的快取線在 CPU7 的快取記憶體中,就會發生以下經過簡化的事件序列:

  • CPU0 檢查本地快取記憶體,沒有找到快取線。

  • 請求被轉發到 CPU0 和 CPU1 的互聯模組,檢查 CPU1 的本地快取記憶體,沒有找到快取線。

  • 請求被轉發到系統互聯模組,檢查其他三個管芯,得知快取線被 CPU6和 CPU7 所在的管芯持有。

  • 請求被轉發到 CPU6 和 CPU7 的互聯模組,檢查這兩個 CPU 的快取記憶體,在 CPU7 的快取記憶體中找到快取線。

  • CPU7 將快取線傳送給所屬的互聯模組,並且重新整理自己快取記憶體中的快取線。

  • CPU6 和 CPU7 的互聯模組將快取線傳送給系統互聯模組。

  • 系統互聯模組將快取線傳送給 CPU0 和 CPU1 的互聯模組。

  • CPU0 和 CPU1 的互聯模組將快取線傳送給 CPU0 的快取記憶體。

  • CPU0 現在可以對快取記憶體中的變數執行 CAS 操作了

所以,就存在這種快取線遷移的情況,造成CAS開銷。