1. 程式人生 > >多執行緒之原子變數CAS演算法(二)

多執行緒之原子變數CAS演算法(二)

上篇博文,我們介紹了多執行緒之記憶體可見性Volatile(一),但是也遺留了一個問題,如何保證變數的”原子性操作(Atomic operations)”?

Volatile保證部分型別的原子性

上篇博文,我們說Voloatile不能保證原子性,有一點侷限:
因為在32位(4位元組)處理器中,Java中讀取long型別變數不是原子的,需要分成兩步,如果一個執行緒正在修改該long變數的值,另一個執行緒可能只能看到該值的一半(前32位)。但是對一個volatile型的long或double變數的讀寫時原子的。詳解

這篇博文,我們給出另外一個解決方案:原子變數CAS演算法。

CAS演算法

CAS(Compare-And-Swap)是一種硬體對併發的支援,針對多處理器操作而設計的,處理器中的一種特殊指令,用於管理對共享資料的併發訪問。

CAS是一種無鎖的非阻塞演算法實現。是硬體對於併發操作的支援,保證了資料變數的原子性。

Cas包含了3個運算元:

  1. 記憶體值 V
  2. 預估值 A
  3. 更新值 B

當且僅當 V == A 時, V = B; 否則,不會執行任何操作。

簡單的來說,CAS有3個運算元,要讀寫的記憶體值V,舊的預期值A,要修改的新值B。當且僅當預期值A和記憶體值V相同時,將記憶體值V修改為B,否則返回V。這是一種樂觀鎖的思路,它相信在它修改之前,沒有其它執行緒去修改它。

原子性問題

在資料庫中事務必須要有原子性,它所做的對資料改操作要全部執行,要麼全部不執行。此時的原子性是相同的概念。
我們看一下i++的原子性問題

i++的原子問題

i++的操作實際上分為三個步驟”讀-改-寫”。

int i = 10;
i = i++; //10

//i++,實際上執行了下面三步:
int temp = i;
i = i + 1;
i = temp;

只有這三步同時執行成功或失敗,就是一個原子操作。

java利用CAS實現原子性

我們知道在java.util.concurrent.atomic包下,java利用CAS演算法給我們提供原子操作的類:

  1. 類AtomicBoolean、AtomicInteger、AtomicLong和AtomicReference的例項各自提供了對相應型別單個變數的訪問和更新。每個類也為該型別提供適當的實用工具方法。
  2. AtomicIntegerArray、AtomicLongArray和AtomicReferenceArray類進一步擴充套件了原子操作,對這些型別的陣列提供了支援。這些類在為其陣列元素提供volatile訪問語義方面也引人注目,這對於普通陣列來說是不受支援的。
  3. 核心方法
 public final boolean compareAndSet(long expect, long update)

下面我們看一個簡單例項:

簡單例子

public class TestAtomicDemo {

    public static void main(String[] args) {

        AtomicDemo ad = new AtomicDemo();

        for(int i =0;i<10;i++){
            new Thread(ad).start();
        }
    }
}

class AtomicDemo implements Runnable{

    private AtomicInteger serialNumber = new AtomicInteger();

    @Override
    public void run() {

        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(getSerialNumber());
    }

    public int getSerialNumber(){
        return serialNumber.getAndIncrement();
    }
}

執行結果:
這裡寫圖片描述

10子執行緒併發訪問serialNumber,不斷執行加1操作,保證原子性。原理圖:
這裡寫圖片描述

總結

可以用CAS在無鎖的情況下實現原子操作,但要明確應用場合,非常簡單的操作且又不想引入鎖可以考慮使用CAS操作,當想要非阻塞地完成某一操作也可以考慮CAS。不推薦在複雜操作中引入CAS,會使程式可讀性變差,且難以測試,同時會出現問題。

下篇博文我們介紹建立執行緒的方式之一:實現Callable介面