面試必問(wèn)的CAS,要多了解

占小狼 轉(zhuǎn)載請(qǐng)注明原創(chuàng)出處,謝謝!

前言

CAS(Compare and Swap),即比較并替換,實(shí)現(xiàn)并發(fā)算法時(shí)常用到的一種技術(shù),Doug lea大神在java同步器中大量使用了CAS技術(shù),鬼斧神工的實(shí)現(xiàn)了多線程執(zhí)行的安全性。

CAS的思想很簡(jiǎn)單:三個(gè)參數(shù),一個(gè)當(dāng)前內(nèi)存值V、舊的預(yù)期值A(chǔ)、即將更新的值B,當(dāng)且僅當(dāng)預(yù)期值A(chǔ)和內(nèi)存值V相同時(shí),將內(nèi)存值修改為B并返回true,否則什么都不做,并返回false。

問(wèn)題

一個(gè)n++的問(wèn)題。

public class Case {

    public volatile int n;

    public void add() {
        n++;
    }
}

通過(guò)javap -verbose Case看看add方法的字節(jié)碼指令

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++被拆分成了幾個(gè)指令:

  1. 執(zhí)行getfield拿到原始n;
  2. 執(zhí)行iadd進(jìn)行加1操作;
  3. 執(zhí)行putfield寫把累加后的值寫回n;

通過(guò)volatile修飾的變量可以保證線程之間的可見性,但并不能保證這3個(gè)指令的原子執(zhí)行,在多線程并發(fā)執(zhí)行下,無(wú)法做到線程安全,得到正確的結(jié)果,那么應(yīng)該如何解決呢?

如何解決

add方法加上synchronized修飾解決。

public class Case {

    public volatile int n;

    public synchronized void add() {
        n++;
    }
}

這個(gè)方案當(dāng)然可行,但是性能上差了點(diǎn),還有其它方案么?

再來(lái)看一段代碼

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

如果這段代碼在并發(fā)下執(zhí)行,會(huì)發(fā)生什么?

假設(shè)線程1和線程2都過(guò)了a==1的檢測(cè),都準(zhǔn)備執(zhí)行對(duì)a進(jìn)行賦值,結(jié)果就是兩個(gè)線程同時(shí)修改了變量a,顯然這種結(jié)果是無(wú)法符合預(yù)期的,無(wú)法確定a的最終值。

解決方法也同樣暴力,在compareAndSwapInt方法加鎖同步,變成一個(gè)原子操作,同一時(shí)刻只有一個(gè)線程才能修改變量a。

除了低性能的加鎖方案,我們還可以使用JDK自帶的CAS方案,在CAS中,比較和替換是一組原子操作,不會(huì)被外部打斷,且在性能上更占有優(yōu)勢(shì)。

下面以AtomicInteger的實(shí)現(xiàn)為例,分析一下CAS是如何實(shí)現(xiàn)的。

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方法無(wú)法直接訪問(wèn)底層系統(tǒng),需要通過(guò)本地(native)方法來(lái)訪問(wèn),Unsafe相當(dāng)于一個(gè)后門,基于該類可以直接操作特定內(nèi)存的數(shù)據(jù)。
  2. 變量valueOffset,表示該變量值在內(nèi)存中的偏移地址,因?yàn)閁nsafe就是根據(jù)內(nèi)存偏移地址獲取數(shù)據(jù)的。
  3. 變量value用volatile修飾,保證了多線程之間的內(nèi)存可見性。

看看AtomicInteger如何實(shí)現(xiàn)并發(fā)下的累加操作:

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

假設(shè)線程A和線程B同時(shí)執(zhí)行g(shù)etAndAdd操作(分別跑在不同CPU上):

  1. AtomicInteger里面的value原始值為3,即主內(nèi)存中AtomicInteger的value為3,根據(jù)Java內(nèi)存模型,線程A和線程B各自持有一份value的副本,值為3。
  2. 線程A通過(guò)getIntVolatile(var1, var2)拿到value值3,這時(shí)線程A被掛起。
  3. 線程B也通過(guò)getIntVolatile(var1, var2)方法獲取到value值3,運(yùn)氣好,線程B沒有被掛起,并執(zhí)行compareAndSwapInt方法比較內(nèi)存值也為3,成功修改內(nèi)存值為2。
  4. 這時(shí)線程A恢復(fù),執(zhí)行compareAndSwapInt方法比較,發(fā)現(xiàn)自己手里的值(3)和內(nèi)存的值(2)不一致,說(shuō)明該值已經(jīng)被其它線程提前修改過(guò)了,那只能重新來(lái)一遍了。
  5. 重新獲取value值,因?yàn)樽兞縱alue被volatile修飾,所以其它線程對(duì)它的修改,線程A總是能夠看到,線程A繼續(xù)執(zhí)行compareAndSwapInt進(jìn)行比較替換,直到成功。

整個(gè)過(guò)程中,利用CAS保證了對(duì)于value的修改的并發(fā)安全,繼續(xù)深入看看Unsafe類中的compareAndSwapInt方法實(shí)現(xiàn)。

public final native boolean compareAndSwapInt(Object paramObject, long paramLong, int paramInt1, int paramInt2);

Unsafe類中的compareAndSwapInt,是一個(gè)本地方法,該方法的實(shí)現(xiàn)位于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
  1. 先想辦法拿到變量value在內(nèi)存中的地址。
  2. 通過(guò)Atomic::cmpxchg實(shí)現(xiàn)比較替換,其中參數(shù)x是即將更新的值,參數(shù)e是原內(nèi)存的值。

如果是Linux的x86,Atomic::cmpxchg方法的實(shí)現(xiàn)如下:

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

看到這匯編,內(nèi)心崩潰 ??

__asm__表示匯編的開始
volatile表示禁止編譯器優(yōu)化
LOCK_IF_MP是個(gè)內(nèi)聯(lián)函數(shù)

#define LOCK_IF_MP(mp) "cmp $0, " #mp "; je 1f; lock; 1: "

Window的x86實(shí)現(xiàn)如下:

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根據(jù)當(dāng)前系統(tǒng)是否為多核處理器決定是否為cmpxchg指令添加lock前綴。

  1. 如果是多處理器,為cmpxchg指令添加lock前綴。
  2. 反之,就省略lock前綴。(單處理器會(huì)不需要lock前綴提供的內(nèi)存屏障效果)

intel手冊(cè)對(duì)lock前綴的說(shuō)明如下:

  1. 確保后續(xù)指令執(zhí)行的原子性。
    在Pentium及之前的處理器中,帶有l(wèi)ock前綴的指令在執(zhí)行期間會(huì)鎖住總線,使得其它處理器暫時(shí)無(wú)法通過(guò)總線訪問(wèn)內(nèi)存,很顯然,這個(gè)開銷很大。在新的處理器中,Intel使用緩存鎖定來(lái)保證指令執(zhí)行的原子性,緩存鎖定將大大降低lock前綴指令的執(zhí)行開銷。
  2. 禁止該指令與前面和后面的讀寫指令重排序。
  3. 把寫緩沖區(qū)的所有數(shù)據(jù)刷新到內(nèi)存中。

上面的第2點(diǎn)和第3點(diǎn)所具有的內(nèi)存屏障效果,保證了CAS同時(shí)具有volatile讀和volatile寫的內(nèi)存語(yǔ)義。

CAS缺點(diǎn)

CAS存在一個(gè)很明顯的問(wèn)題,即ABA問(wèn)題。

問(wèn)題:如果變量V初次讀取的時(shí)候是A,并且在準(zhǔn)備賦值的時(shí)候檢查到它仍然是A,那能說(shuō)明它的值沒有被其他線程修改過(guò)了嗎?

如果在這段期間曾經(jīng)被改成B,然后又改回A,那CAS操作就會(huì)誤認(rèn)為它從來(lái)沒有被修改過(guò)。針對(duì)這種情況,java并發(fā)包中提供了一個(gè)帶有標(biāo)記的原子引用類AtomicStampedReference,它可以通過(guò)控制變量值的版本來(lái)保證CAS的正確性。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容