多執行緒之原子變數CAS演算法(二)
上篇博文,我們介紹了多執行緒之記憶體可見性Volatile(一),但是也遺留了一個問題,如何保證變數的”原子性操作(Atomic operations)”?
Volatile保證部分型別的原子性
上篇博文,我們說Voloatile不能保證原子性,有一點侷限:
因為在32位(4位元組)處理器中,Java中讀取long型別變數不是原子的,需要分成兩步,如果一個執行緒正在修改該long變數的值,另一個執行緒可能只能看到該值的一半(前32位)。但是對一個volatile型的long或double變數的讀寫時原子的。詳解
這篇博文,我們給出另外一個解決方案:原子變數CAS演算法。
CAS演算法
CAS(Compare-And-Swap)是一種硬體對併發的支援,針對多處理器操作而設計的,處理器中的一種特殊指令,用於管理對共享資料的併發訪問。
CAS是一種無鎖的非阻塞演算法實現。是硬體對於併發操作的支援,保證了資料變數的原子性。
Cas包含了3個運算元:
- 記憶體值 V
- 預估值 A
- 更新值 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演算法給我們提供原子操作的類:
- 類AtomicBoolean、AtomicInteger、AtomicLong和AtomicReference的例項各自提供了對相應型別單個變數的訪問和更新。每個類也為該型別提供適當的實用工具方法。
- AtomicIntegerArray、AtomicLongArray和AtomicReferenceArray類進一步擴充套件了原子操作,對這些型別的陣列提供了支援。這些類在為其陣列元素提供volatile訪問語義方面也引人注目,這對於普通陣列來說是不受支援的。
- 核心方法
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介面