CAS(Compare and Swap),即比較並替換,實現併發算法時經常使用到的一種技術,Doug lea大神在java同步器中大量使用了CAS技術,鬼斧神工的實現了多線程執行的安全性。php
CAS的思想很簡單:三個參數,一個當前內存值V、舊的預期值A、即將更新的值B,當且僅當預期值A和內存值V相同時,將內存值修改成B並返回true,不然什麼都不作,並返回false。java
一個n++
的問題。算法
public class Case { public volatile int n; public void add() { n++; } }
經過javap -verbose Case
看看add方法的字節碼指令緩存
public void add(); flags: ACC_PUBLIC Code: stack=3, locals=1, args_size=1 0: aload_0 1: dup 2: getfield #2 // Field n:I 5: iconst_1 6: iadd 7: putfield #2 // Field n:I 10: return
n++
被拆分紅了幾個指令:安全
getfield
拿到原始n;iadd
進行加1操做;putfield
寫把累加後的值寫回n;經過volatile修飾的變量能夠保證線程之間的可見性,但並不能保證這3個指令的原子執行,在多線程併發執行下,沒法作到線程安全,獲得正確的結果,那麼應該如何解決呢?多線程
在add
方法加上synchronized修飾解決。併發
public class Case { public volatile int n; public synchronized void add() { n++; } }
這個方案固然可行,可是性能上差了點,還有其它方案麼?app
再來看一段代碼函數
public int a = 1; public boolean compareAndSwapInt(int b) { if (a == 1) { a = b; return true; } return false; }
若是這段代碼在併發下執行,會發生什麼?oop
假設線程1和線程2都過了a==1
的檢測,都準備執行對a進行賦值,結果就是兩個線程同時修改了變量a,顯然這種結果是沒法符合預期的,沒法肯定a的最終值。
解決方法也一樣暴力,在compareAndSwapInt方法加鎖同步,變成一個原子操做,同一時刻只有一個線程才能修改變量a。
除了低性能的加鎖方案,咱們還可使用JDK自帶的CAS方案,在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;} }
看看AtomicInteger
如何實現併發下的累加操做:
public final int getAndAdd(int delta) { return unsafe.getAndAddInt(this, valueOffset, delta); } //unsafe.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; }
假設線程A和線程B同時執行getAndAdd操做(分別跑在不一樣CPU上):
getIntVolatile(var1, var2)
拿到value值3,這時線程A被掛起。getIntVolatile(var1, var2)
方法獲取到value值3,運氣好,線程B沒有被掛起,並執行compareAndSwapInt
方法比較內存值也爲3,成功修改內存值爲2。compareAndSwapInt
方法比較,發現本身手裏的值(3)和內存的值(2)不一致,說明該值已經被其它線程提早修改過了,那隻能從新來一遍了。compareAndSwapInt
進行比較替換,直到成功。整個過程當中,利用CAS保證了對於value的修改的併發安全,繼續深刻看看Unsafe類中的compareAndSwapInt方法實現。
public final native boolean compareAndSwapInt(Object paramObject, long paramLong, int paramInt1, int paramInt2);
Unsafe類中的compareAndSwapInt,是一個本地方法,該方法的實現位於unsafe.cpp
中
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
Atomic::cmpxchg
實現比較替換,其中參數x是即將更新的值,參數e是原內存的值。若是是Linux的x86,Atomic::cmpxchg
方法的實現以下:
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; }
看到這彙編,心裏崩潰 😖
__asm__
表示彙編的開始
volatile
表示禁止編譯器優化
LOCK_IF_MP
是個內聯函數
#define LOCK_IF_MP(mp) "cmp $0, " #mp "; je 1f; lock; 1: "
Window的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 } } // Adding a lock prefix to an instruction on MP machine // VC++ doesn't like the lock prefix to be on a single line // so we can't insert a label after the lock prefix. // By emitting a lock prefix, we can define a label after it. #define LOCK_IF_MP(mp) __asm cmp mp, 0 \ __asm je L0 \ __asm _emit 0xF0 \ __asm L0:
LOCK_IF_MP
根據當前系統是否爲多核處理器決定是否爲cmpxchg指令添加lock前綴。
intel手冊對lock前綴的說明以下:
上面的第2點和第3點所具備的內存屏障效果,保證了CAS同時具備volatile讀和volatile寫的內存語義。
CAS存在一個很明顯的問題,即ABA問題。
問題:若是變量V初次讀取的時候是A,而且在準備賦值的時候檢查到它仍然是A,那能說明它的值沒有被其餘線程修改過了嗎?
若是在這段期間曾經被改爲B,而後又改回A,那CAS操做就會誤認爲它歷來沒有被修改過。針對這種狀況,java併發包中提供了一個帶有標記的原子引用類AtomicStampedReference
,它能夠經過控制變量值的版原本保證CAS的正確性。
做者:佔小狼連接:https://www.jianshu.com/p/fb6e91b013cc來源:簡書著做權歸做者全部。商業轉載請聯繫做者得到受權,非商業轉載請註明出處。