Java併發編程-CAS

CAS,Compare and Swap即比較並替換,設計併發算法時經常使用到的一種技術,Doug lea大神在java同步器中大量使用了CAS技術,鬼斧神工的實現了多線程執行的安全性。java

目前的處理器基本都支持CAS,只不過不一樣的廠家的實現不同罷了。CAS有三個操做數:內存值V、舊的預期值A、要修改的值B,當且僅當預期值A和內存值V相同時,將內存值修改成B並返回true,不然什麼都不作並返回false。c++

實現分析

先看一段代碼算法

public int a = 1;
public boolean compareAndSwapInt(int b) {
    if (a == 1) {
        a = b;
        return true;
    }
    return false;
}

試想這段代碼在多線程併發下,會發生什麼?咱們不妨來分析一下:緩存

  1. 線程A執行到 a==1,正準備執行a = b時,線程B也正在運行a = b,並在線程A以前把a修改成2;最後線程A又把a修改爲了3。結果就是兩個線程同時修改了變量a,顯然這種結果是沒法符合預期的,沒法肯定a的值。
  2. 解決方法也很簡單,在compareAndSwapInt方法加鎖同步,變成一個原子操做,同一時刻只有一個線程才能修改變量a。

CAS中的比較和替換是一組原子操做,不會被外部打斷,先根據paramLong/paramLong1獲取到內存當中當前的內存值V,在將內存值V和原值A做比較,要是相等就修改成要修改的值B,屬於硬件級別的操做,效率比加鎖操做高。安全

java.util.concurrent.atomic包下的原子操做類都是基於CAS實現的,接下去咱們經過AtomicInteger來看看是如何經過CAS實現原子操做的:多線程

public class AtomicInteger extends Number implements java.io.Serializable {
    // setup to use Unsafe.compareAndSwapInt for updates
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    private volatile int value;
    public final int get() {return value;}
}
  1. Unsafe是CAS的核心類,Java沒法直接訪問底層操做系統,而是經過本地(native)方法來訪問。不過儘管如此,JVM仍是開了一個後門,JDK中有一個類Unsafe,它提供了硬件級別的原子操做。
  2. valueOffset表示的是變量值在內存中的偏移地址,由於Unsafe就是根據內存偏移地址獲取數據的原值的。
  3. value是用volatile修飾的,保證了多線程之間看到的value值是同一份。

接下去,咱們看看AtomicInteger是如何實現併發下的累加操做:併發

//jdk1.8實現
public final int getAndAdd(int delta) {    
    return unsafe.getAndAddInt(this, valueOffset, delta);
}

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

在jdk1.8中,比較和替換操做放在unsafe類中實現。this

假設如今線程A和線程B同時執行getAndAdd操做:atom

  1. AtomicInteger裏面的value原始值爲3,即主內存中AtomicInteger的value爲3,根據Java內存模型,線程A和線程B各自持有一份value的副本,值爲3。
  2. 線程A經過getIntVolatile(var1, var2)方法獲取到value值3,線程切換,線程A掛起。
  3. 線程B經過getIntVolatile(var1, var2)方法獲取到value值3,並利用compareAndSwapInt方法比較內存值也爲3,比較成功,修改內存值爲2,線程切換,線程B掛起。
  4. 線程A恢復,利用compareAndSwapInt方法比較,發手裏的值3和內存值4不一致,此時value正在被另一個線程修改,線程A不能修改value值。
  5. 線程的compareAndSwapInt實現,循環判斷,從新獲取value值,由於value是volatile變量,因此線程對它的修改,線程A老是可以看到。線程A繼續利用compareAndSwapInt進行比較並替換,直到compareAndSwapInt修改爲功返回true。

整個過程當中,利用CAS保證了對於value的修改的線程安全性。spa


咱們繼續深刻看看Unsafe類中的compareAndSwapInt方法。

public final native boolean compareAndSwapInt(Object paramObject, long paramLong, int paramInt1, int paramInt2);
能夠看到,這是一個本地方法調用,這個本地方法在openjdk中依次調用c++代碼:unsafe.cpp,atomic.cpp,atomic_window_x86.inline.hpp。下面是對應於intel X86處理器的源代碼片斷。
inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
    int mp = os::isMP(); //判斷是不是多處理器
    _asm {
        mov edx, dest
        mov ecx, exchange_value
        mov eax, compare_value
        LOCK_IF_MP(mp)
        cmpxchg dword ptr [edx], ecx
    }
}

從上面的源碼中能夠看出,會根據當前處理器類型來決定是否爲cmpxchg指令添加lock前綴。

  1. 若是是多處理器,爲cmpxchg指令添加lock前綴。
  2. 反之,就省略lock前綴。(單處理器會不須要lock前綴提供的內存屏障效果)

intel手冊對lock前綴的說明以下:

  1. 確保對內存讀改寫操做的原子執行。
    在Pentium及以前的處理器中,帶有lock前綴的指令在執行期間會鎖住總線,使得其它處理器暫時沒法經過總線訪問內存,很顯然,這個開銷很大。在新的處理器中,Intel使用緩存鎖定來保證指令執行的原子性。緩存鎖定將大大下降lock前綴指令的執行開銷。
  2. 禁止該指令,與前面和後面的讀寫指令重排序。
  3. 把寫緩衝區的全部數據刷新到內存中。

上面的第2點和第3點所具備的內存屏障效果,保證了CAS同時具備volatile讀和volatile寫的內存語義。

CAS缺點

CAS存在一個很明顯的問題,即ABA問題。若是變量V初次讀取的時候是A,而且在準備賦值的時候檢查到它仍然是A,那能說明它的值沒有被其餘線程修改過了嗎?若是在這段期間它的值曾經被改爲了B,而後又改回A,那CAS操做就會誤認爲它歷來沒有被修改過。針對這種狀況,java併發包中提供了一個帶有標記的原子引用類"AtomicStampedReference",它能夠經過控制變量值的版原本保證CAS的正確性。

相關文章
相關標籤/搜索