面試必問的CAS,要多了解

占小狼 轉(zhuǎn)載請注明原創(chuàng)出處粥喜,謝謝空幻!

前言

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

CAS的思想很簡單:三個參數(shù)但两,一個當前內(nèi)存值V、舊的預(yù)期值A(chǔ)供置、即將更新的值B谨湘,當且僅當預(yù)期值A(chǔ)和內(nèi)存值V相同時,將內(nèi)存值修改為B并返回true芥丧,否則什么都不做紧阔,并返回false。

問題

一個n++的問題续担。

public class Case {

    public volatile int n;

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

通過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++被拆分成了幾個指令:

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

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

如何解決

add方法加上synchronized修飾解決警儒。

public class Case {

    public volatile int n;

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

這個方案當然可行,但是性能上差了點眶根,還有其它方案么蜀铲?

再來看一段代碼

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

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

假設(shè)線程1和線程2都過了a==1的檢測蝙茶,都準備執(zhí)行對a進行賦值,結(jié)果就是兩個線程同時修改了變量a诸老,顯然這種結(jié)果是無法符合預(yù)期的隆夯,無法確定a的最終值。

解決方法也同樣暴力别伏,在compareAndSwapInt方法加鎖同步蹄衷,變成一個原子操作,同一時刻只有一個線程才能修改變量a厘肮。

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

下面以AtomicInteger的實現(xiàn)為例厚骗,分析一下CAS是如何實現(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方法無法直接訪問底層系統(tǒng)夫嗓,需要通過本地(native)方法來訪問,Unsafe相當于一個后門冲秽,基于該類可以直接操作特定內(nèi)存的數(shù)據(jù)舍咖。
  2. 變量valueOffset,表示該變量值在內(nèi)存中的偏移地址锉桑,因為Unsafe就是根據(jù)內(nèi)存偏移地址獲取數(shù)據(jù)的排霉。
  3. 變量value用volatile修飾,保證了多線程之間的內(nèi)存可見性刨仑。

看看AtomicInteger如何實現(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同時執(zhí)行g(shù)etAndAdd操作(分別跑在不同CPU上):

  1. AtomicInteger里面的value原始值為3郑诺,即主內(nèi)存中AtomicInteger的value為3,根據(jù)Java內(nèi)存模型杉武,線程A和線程B各自持有一份value的副本辙诞,值為3。
  2. 線程A通過getIntVolatile(var1, var2)拿到value值3轻抱,這時線程A被掛起飞涂。
  3. 線程B也通過getIntVolatile(var1, var2)方法獲取到value值3,運氣好祈搜,線程B沒有被掛起较店,并執(zhí)行compareAndSwapInt方法比較內(nèi)存值也為3,成功修改內(nèi)存值為2容燕。
  4. 這時線程A恢復(fù)梁呈,執(zhí)行compareAndSwapInt方法比較,發(fā)現(xiàn)自己手里的值(3)和內(nèi)存的值(2)不一致蘸秘,說明該值已經(jīng)被其它線程提前修改過了官卡,那只能重新來一遍了。
  5. 重新獲取value值醋虏,因為變量value被volatile修飾寻咒,所以其它線程對它的修改,線程A總是能夠看到颈嚼,線程A繼續(xù)執(zhí)行compareAndSwapInt進行比較替換毛秘,直到成功。

整個過程中,利用CAS保證了對于value的修改的并發(fā)安全叫挟,繼續(xù)深入看看Unsafe類中的compareAndSwapInt方法實現(xiàn)艰匙。

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

Unsafe類中的compareAndSwapInt,是一個本地方法霞揉,該方法的實現(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. 通過Atomic::cmpxchg實現(xiàn)比較替換,其中參數(shù)x是即將更新的值适秩,參數(shù)e是原內(nèi)存的值。

如果是Linux的x86硕舆,Atomic::cmpxchg方法的實現(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是個內(nèi)聯(lián)函數(shù)

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

Window的x86實現(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ù)當前系統(tǒng)是否為多核處理器決定是否為cmpxchg指令添加lock前綴。

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

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

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

上面的第2點和第3點所具有的內(nèi)存屏障效果扮饶,保證了CAS同時具有volatile讀和volatile寫的內(nèi)存語義具练。

CAS缺點

CAS存在一個很明顯的問題,即ABA問題甜无。

問題:如果變量V初次讀取的時候是A扛点,并且在準備賦值的時候檢查到它仍然是A,那能說明它的值沒有被其他線程修改過了嗎毫蚓?

如果在這段期間曾經(jīng)被改成B占键,然后又改回A,那CAS操作就會誤認為它從來沒有被修改過元潘。針對這種情況畔乙,java并發(fā)包中提供了一個帶有標記的原子引用類AtomicStampedReference,它可以通過控制變量值的版本來保證CAS的正確性翩概。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末牲距,一起剝皮案震驚了整個濱河市返咱,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌牍鞠,老刑警劉巖咖摹,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異难述,居然都是意外死亡萤晴,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進店門胁后,熙熙樓的掌柜王于貴愁眉苦臉地迎上來店读,“玉大人,你說我怎么就攤上這事攀芯⊥投希” “怎么了?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵侣诺,是天一觀的道長殖演。 經(jīng)常有香客問我,道長年鸳,這世上最難降的妖魔是什么趴久? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮阻星,結(jié)果婚禮上朋鞍,老公的妹妹穿的比我還像新娘。我一直安慰自己妥箕,他們只是感情好滥酥,可當我...
    茶點故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著畦幢,像睡著了一般坎吻。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上宇葱,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天瘦真,我揣著相機與錄音,去河邊找鬼黍瞧。 笑死诸尽,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的印颤。 我是一名探鬼主播您机,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了际看?” 一聲冷哼從身側(cè)響起咸产,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎仲闽,沒想到半個月后脑溢,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡赖欣,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年屑彻,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片畏鼓。...
    茶點故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡酱酬,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出云矫,到底是詐尸還是另有隱情,我是刑警寧澤汗菜,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布让禀,位于F島的核電站,受9級特大地震影響陨界,放射性物質(zhì)發(fā)生泄漏巡揍。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一菌瘪、第九天 我趴在偏房一處隱蔽的房頂上張望腮敌。 院中可真熱鬧,春花似錦俏扩、人聲如沸糜工。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽捌木。三九已至,卻和暖如春嫉戚,著一層夾襖步出監(jiān)牢的瞬間刨裆,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工彬檀, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留帆啃,地道東北人。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓窍帝,卻偏偏與公主長得像努潘,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,786評論 2 345

推薦閱讀更多精彩內(nèi)容