CAS底層原理

在Java并發(fā)中户侥,我們最初接觸的應(yīng)該就是synchronized關(guān)鍵字了,但是synchronized屬于重量級鎖峦嗤,很多時候會引起性能問題蕊唐,volatile也是個不錯的選擇,但是volatile不能保證原子性烁设,只能在某些場合下使用替梨。

像synchronized這種獨占鎖屬于悲觀鎖,它是在假設(shè)一定會發(fā)生沖突的装黑,那么加鎖恰好有用副瀑,除此之外,還有樂觀鎖恋谭,樂觀鎖的含義就是假設(shè)沒有發(fā)生沖突糠睡,那么我正好可以進行某項操作,如果要是發(fā)生沖突呢箕别,那我就重試直到成功铜幽,樂觀鎖最常見的就是CAS。

我們在讀Concurrent包下的類的源碼時串稀,發(fā)現(xiàn)無論是ReenterLock內(nèi)部的AQS除抛,還是各種Atomic開頭的原子類,內(nèi)部都應(yīng)用到了CAS母截,最常見的就是我們在并發(fā)編程時遇到的i++這種情況到忽。傳統(tǒng)的方法肯定是在方法上加上synchronized關(guān)鍵字:

public class Test {

    public volatile int i;

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

但是這種方法在性能上可能會差一點,我們還可以使用AtomicInteger清寇,就可以保證i原子的++了喘漏。

public class Test {

    public AtomicInteger i;

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

我們來看getAndIncrement的內(nèi)部:

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這個函數(shù),它也是CAS縮寫的由來华烟。那么仔細(xì)分析下這個函數(shù)做了什么呢翩迈?

首先我們發(fā)現(xiàn)compareAndSwapInt前面的this,那么它屬于哪個類呢盔夜,我們看上一步getAndAddInt负饲,前面是unsafe堤魁。這里我們進入的Unsafe類。這里要對Unsafe類做個說明返十。結(jié)合AtomicInteger的定義來說:

public class AtomicInteger extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 6214790243416807050L;
    
    // 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;
    ...

在AtomicInteger數(shù)據(jù)定義的部分妥泉,我們可以看到,其實實際存儲的值是放在value中的洞坑,除此之外我們還獲取了unsafe實例盲链,并且定義了valueOffset。再看到static塊迟杂,懂類加載過程的都知道刽沾,static塊的加載發(fā)生于類加載的時候,是最先初始化的逢慌,這時候我們調(diào)用unsafe的objectFieldOffset從Atomic類文件中獲取value的偏移量悠轩,那么valueOffset其實就是記錄value的偏移量的。

再回到上面一個函數(shù)getAndAddInt攻泼,我們看var5獲取的是什么火架,通過調(diào)用unsafe的getIntVolatile(var1, var2),這是個native方法忙菠,具體實現(xiàn)到JDK源碼里去看了何鸡,其實就是獲取var1中,var2偏移量處的值牛欢。var1就是AtomicInteger骡男,var2就是我們前面提到的valueOffset,這樣我們就從內(nèi)存里獲取到現(xiàn)在valueOffset處的值了。

現(xiàn)在重點來了傍睹,compareAndSwapInt(var1, var2, var5, var5 + var4)其實換成compareAndSwapInt(obj, offset, expect, update)比較清楚隔盛,意思就是如果obj內(nèi)的value和expect相等,就證明沒有其他線程改變過這個變量拾稳,那么就更新它為update吮炕,如果這一步的CAS沒有成功,那就采用自旋的方式繼續(xù)進行CAS操作访得,取出乍一看這也是兩個步驟了啊龙亲,其實在JNI里是借助于一個CPU指令完成的。所以還是原子操作悍抑。

CAS底層原理

CAS底層使用JNI調(diào)用C代碼實現(xiàn)的鳄炉,如果你有Hotspot源碼,那么在Unsafe.cpp里可以找到它的實現(xiàn):

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實現(xiàn)是在Unsafe_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處的地址,最后調(diào)用了Atomic::cmpxchg(x, addr, e), 其中參數(shù)x是即將更新的值记靡,參數(shù)e是原內(nèi)存的值磕仅。代碼中能看到cmpxchg有基于各個平臺的實現(xiàn)珊豹,這里選擇Linux X86平臺下的源碼分析:

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說明是ASM匯編榕订,volatile禁止編譯器優(yōu)化

// Adding a lock prefix to an instruction on MP machine
#define LOCK_IF_MP(mp) "cmp $0, " #mp "; je 1f; lock; 1: "

os::is_MP判斷當(dāng)前系統(tǒng)是否為多核系統(tǒng),如果是就給總線加鎖蜕便,所以同一芯片上的其他處理器就暫時不能通過總線訪問內(nèi)存劫恒,保證了該指令在多處理器環(huán)境下的原子性。

在正式解讀這段匯編前轿腺,我們來了解下嵌入?yún)R編的基本格式:

asm ( assembler template
    : output operands                  /* optional */
    : input operands                   /* optional */
    : list of clobbered registers      /* optional */
    );

  • template就是cmpxchgl %1,(%3)表示匯編模板
  • output operands表示輸出操作數(shù),=a對應(yīng)eax寄存器
  • input operand 表示輸入?yún)?shù)两嘴,%1 就是exchange_value, %3是dest, %4就是mp, r表示任意寄存器族壳,a還是eax寄存器
  • list of clobbered registers就是些額外參數(shù)憔辫,cc表示編譯器cmpxchgl的執(zhí)行將影響到標(biāo)志寄存器, memory告訴編譯器要重新從內(nèi)存中讀取變量的最新值,這點實現(xiàn)了volatile的感覺仿荆。

那么表達式其實就是cmpxchgl exchange_value ,dest贰您,我們會發(fā)現(xiàn)%2也就是compare_value沒有用上,這里就要分析cmpxchgl的語義了拢操。cmpxchgl末尾l表示操作數(shù)長度為4锦亦,上面已經(jīng)知道了。cmpxchgl會默認(rèn)比較eax寄存器的值即compare_value和exchange_value的值令境,如果相等杠园,就把dest的值賦值給exchange_value,否則,將exchange_value賦值給eax

最終舔庶,JDK通過CPU的cmpxchgl指令的支持抛蚁,實現(xiàn)AtomicInteger的CAS操作的原子性。

CAS 的問題

ABA問題

CAS需要在操作值的時候檢查下值有沒有發(fā)生變化惕橙,如果沒有發(fā)生變化則更新瞧甩,但是如果一個值原來是A,變成了B吕漂,又變成了A亲配,那么使用CAS進行檢查時會發(fā)現(xiàn)它的值沒有發(fā)生變化,但是實際上卻變化了惶凝。這就是CAS的ABA問題吼虎。
常見的解決思路是使用版本號。在變量前面追加上版本號苍鲜,每次變量更新的時候把版本號加一思灰,那么A-B-A 就會變成1A-2B-3A。
目前在JDK的atomic包里提供了一個類AtomicStampedReference來解決ABA問題混滔。這個類的compareAndSet方法作用是首先檢查當(dāng)前引用是否等于預(yù)期引用洒疚,并且當(dāng)前標(biāo)志是否等于預(yù)期標(biāo)志歹颓,如果全部相等,則以原子方式將該引用和該標(biāo)志的值設(shè)置為給定的更新值油湖。

循環(huán)時間長開銷大
上面我們說過如果CAS不成功巍扛,則會原地自旋,如果長時間自旋會給CPU帶來非常大的執(zhí)行開銷乏德。

為什么cas更快撤奸?

其實可以看出cas本質(zhì)也是加鎖了的,但為啥往往cas比用lock更快喊括,其實lock本質(zhì)也是使用了cas的胧瓜,但當(dāng)競爭比較激烈時,它會有線程的阻塞郑什,掛起等府喳,cas顯然不會有。它更輕量級蘑拯。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末钝满,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子强胰,更是在濱河造成了極大的恐慌舱沧,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,427評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件偶洋,死亡現(xiàn)場離奇詭異熟吏,居然都是意外死亡,警方通過查閱死者的電腦和手機玄窝,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評論 3 395
  • 文/潘曉璐 我一進店門牵寺,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人恩脂,你說我怎么就攤上這事帽氓。” “怎么了俩块?”我有些...
    開封第一講書人閱讀 165,747評論 0 356
  • 文/不壞的土叔 我叫張陵黎休,是天一觀的道長。 經(jīng)常有香客問我玉凯,道長势腮,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,939評論 1 295
  • 正文 為了忘掉前任漫仆,我火速辦了婚禮捎拯,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘盲厌。我一直安慰自己署照,他們只是感情好祸泪,可當(dāng)我...
    茶點故事閱讀 67,955評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著建芙,像睡著了一般没隘。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上岁钓,一...
    開封第一講書人閱讀 51,737評論 1 305
  • 那天升略,我揣著相機與錄音,去河邊找鬼屡限。 笑死,一個胖子當(dāng)著我的面吹牛炕倘,可吹牛的內(nèi)容都是我干的钧大。 我是一名探鬼主播,決...
    沈念sama閱讀 40,448評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼罩旋,長吁一口氣:“原來是場噩夢啊……” “哼啊央!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起涨醋,我...
    開封第一講書人閱讀 39,352評論 0 276
  • 序言:老撾萬榮一對情侶失蹤瓜饥,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后浴骂,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體乓土,經(jīng)...
    沈念sama閱讀 45,834評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,992評論 3 338
  • 正文 我和宋清朗相戀三年溯警,在試婚紗的時候發(fā)現(xiàn)自己被綠了趣苏。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,133評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡梯轻,死狀恐怖食磕,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情喳挑,我是刑警寧澤彬伦,帶...
    沈念sama閱讀 35,815評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站伊诵,受9級特大地震影響单绑,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜日戈,卻給世界環(huán)境...
    茶點故事閱讀 41,477評論 3 331
  • 文/蒙蒙 一询张、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧浙炼,春花似錦份氧、人聲如沸唯袄。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,022評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽恋拷。三九已至,卻和暖如春厅缺,著一層夾襖步出監(jiān)牢的瞬間蔬顾,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,147評論 1 272
  • 我被黑心中介騙來泰國打工湘捎, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留诀豁,地道東北人。 一個月前我還...
    沈念sama閱讀 48,398評論 3 373
  • 正文 我出身青樓窥妇,卻偏偏與公主長得像舷胜,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子活翩,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,077評論 2 355