CAS原子性操作

概念

CAS(compare and swap),比較和交換,是原子操作的一種,可用於在多線程編程中實現不被打斷的數據交換操作,從而避免多線程同時改寫某一數據時由於執行順序不確定性以及中斷的不可預知性產生的數據不一致問題。 該操作通過將內存中的值與指定數據進行比較,當數值一樣時將內存中的數據替換爲新的值
現代的大多數CPU都實現了CAS,它是一種==無鎖==(lock-free),且==非阻塞==的一種算法,保持數據的一致性

1.java中的原子性操作

1.1java如何實現原子性操作的

    在java中通過鎖和循環cas的方式實現原子操作

2.1cas是如何實現的

    jvm中的CAS操作是基於處理器的CMPXCHG指令實現的
CAS有三個操作數:
內存值V、舊的預期值A、要修改的值B。,
當且僅當預期值A和內存值V相同時,將內存值修改爲B並返回true,
否則什麼都不做並返回false
實現分析:
public int a = 1;
public boolean compareAndSwapInt(int b) {
    if (a == 1) {
        a = b;
        return true;
    }
    return false;
}

cas步驟還原:

  • 需要傳遞三個參數 1.當前線程中獲取的舊值 2.新值 3.內存地址中的值
  • 循環比較舊值和內存地址中的值,直到比較成功爲止,即使失敗,舊值是用volatile修飾的,保證一旦發生改變,能夠被其他線程所察覺.然後再進行比較.

2.1.1CAS存在的3個問題

        ABA問題 : 當兩個線程同時操作數據A時,當線程1操作數據A之後將值改爲了B,後面又改成了A,這時候Cas默認還是值是沒有改變的

java中的AtomicInteger的原子操作

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類中實現。

假設現在線程A和線程B同時執行getAndAdd操作:

  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的修改的線程安全性。

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前綴。
  • 反之,就省略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的正確性。

關於ABA問題參考

參考佔小狼博客

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