多線程之CAS淺析

CAS簡介

比較並交換(compare and swap, CAS),是原子操作的一種,可用於在多線程編程中實現不被打斷的數據交換操作,從而避免多線程同時改寫某一數據時由於執行順序不確定性以及中斷的不可預知性產生的數據不一致問題。該操作通過將內存中的值與指定數據進行比較,當數值一樣時將內存中的數據替換爲新的值。

悲觀鎖

總是假設最壞的情況,每次取數據時都認爲其他線程會修改,所以都會加鎖(讀鎖、寫鎖、行鎖等),當其他線程想要訪問數據時,都需要阻塞掛起。在Java中,synchronized的思想就是悲觀鎖。

樂觀鎖

總是認爲不會產生併發問題,每次去取數據的時候總認爲不會有其他線程對數據進行修改,因此不會上鎖,但是在更新時會判斷其他線程在這之前有沒有對數據進行修改,一般會使用版本號機制或CAS操作實現。java.util.concurrent包中藉助CAS實現了區別於synchronized同步鎖的一種樂觀鎖。

CAS應用

java.util.concurrent包中無論是ReentrantLock內部的AQS,還是各種Atomic開頭的原子類,內部都應用到了CAS,最常見的就是我們在上一篇遇到的i++這種情況。傳統的方法肯定是在方法上加上synchronized關鍵字:

public volatile int i;
public synchronized void add() {
  i++;
}

這種方法在性能上會比較差,我們可以使用java.util.concurrent包下的AtomicInteger,既能保證i++的原子性,又能提高性能。


public AtomicInteger i;
public void add() {
  i.getAndIncrement();
}

在getAndIncrement()方法中會調用Unsafe的getAndAddInt()


public final int getAndIncrement() {
  return unsafe.getAndAddInt(this, valueOffset, 1);
}

再看看getAndAddInt()方法

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

這裏我們見到compareAndSwapInt這個函數,它就是CAS縮寫的由來。

compareAndSwapInt(var1, var2, var5, var5 + var4)其實換成compareAndSwapInt(obj, offset, expect, update)會更好理解,意思就是如果obj內的value和expect相等,就證明沒有其他線程改變過這個變量,那麼就更新它的值爲update,如果這一步的CAS沒有成功,那就採用自旋的方式繼續進行CAS操作,這也是兩個步驟,但是在JNI裏是藉助於一個CPU指令完成的。所以還是原子操作。

CAS原理

CAS機制當中使用了3個基本操作數:內存地址V,舊的預期值A,要修改的新值B。更新一個變量的時候,只有當變量的預期值A和內存地址V當中的實際值相同時,纔會將內存地址V對應的值修改爲B。

我們繼續查看compareAndSwapInt這個方法,在Java中此方法爲native方法。

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

底層使用JNI調用C代碼實現的,如果你有openJDK源碼,那麼在Unsafe.cpp裏可以找到它的實現。


static JNINativeMethod methods_15[] = {
  ...
  {CC"compareAndSwapInt",  CC"("OBJ"J""I""I"")Z", FN_PTR(Unsafe_CompareAndSwapInt)},
  {CC"compareAndSwapLong", CC"("OBJ"J""J""J"")Z", FN_PTR(Unsafe_CompareAndSwapLong)},
  ...
};

可以看到compareAndSwapInt實現是在Unsafe_CompareAndSwapInt裏面。


UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
  UnsafeWrapper("Unsafe_CompareAndSwapInt");
  oop p = JNIHandles::resolve(obj);
  jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
  return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END

p是取出的對象,addr是p中offset處的地址,最後調用了Atomic::cmpxchg(x, addr, e), 其中參數x是即將更新的值,參數e是原內存的值。

inline jint Atomic::cmpxchg(jint exchange_value, volatile jint* dest, jint compare_value) {
  int mp = os::is_MP();
  __asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)"
                    : "=a" (exchange_value)
                    : "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
                    : "cc", "memory");
  return exchange_value;
}

os::is_MP判斷當前系統是否爲多核系統,如果是就給總線加鎖,所以同一芯片上的其他處理器就暫時不能通過總線訪問內存,保證了該指令在多處理器環境下的原子性。

LOCK_IF_MP根據當前處理器的類型來決定是否爲cmpxchg指令添加lock前綴。如果程序是在多處理器上運行,就爲cmpxchg指令加上lock前綴(lockcmpxchg)。反之,如果程序是在單處理器上運行,就省略lock前綴(單處理器自身會維護單處理器內的順序一致性,不需要lock前綴提供的內存屏障效果)。

cmpxchgl會默認比較eax寄存器的值即compare_value和exchange_value的值,如果相等,就把dest的值賦值給exchange_value,否則,將exchange_value賦值給eax。

CAS的問題

1ABA問題

CAS需要在操作值的時候檢查下值有沒有發生變化,如果沒有發生變化則更新,但是如果一個值原來是A,變成了B,又變成了A,那麼使用CAS進行檢查時會發現它的值沒有發生變化,但是實際上卻變化了。這就是CAS的ABA問題。常見的解決思路是使用版本號。在變量前面追加上版本號,每次變量更新的時候把版本號加一,那麼A-B-A 就會變成1A-2B-3A。

 

2循環時間長開銷大

如果CAS不成功,則會原地自旋,如果長時間自旋會給CPU帶來非常大的執行開銷。

更多精彩內容請關注微信公衆號:

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