CAS初探

本文首發(fā):WindCoder

什么是CAS?

全稱:Compare And Swap治唤,翻譯為比較并替換钝计。

CAS機(jī)制當(dāng)中使用了3個(gè)基本操作數(shù):

  • 內(nèi)存地址V
  • 舊的預(yù)期值A(chǔ)
  • 要修改的新值B

當(dāng)且僅當(dāng)變量的預(yù)期值A(chǔ)和內(nèi)存地址V當(dāng)中的實(shí)際值相同時(shí)酿炸,才會(huì)將內(nèi)存地址V對應(yīng)的值修改為B 瘫絮。

例:

1.在內(nèi)存地址V中,存著值為10的變量填硕。

2.此時(shí)線程1想要把變量的值加1麦萤。對線程1來說,舊的預(yù)期值A(chǔ)=10扁眯,要修改的新值B=11壮莹。

3.在線程1提交更新之前,另一個(gè)線程2先行一步姻檀,把內(nèi)存地址V中的變量先更成了11命满。

4.線程1提交更新,首先A和地址V中的實(shí)際值比較绣版,發(fā)現(xiàn)A不等于V的實(shí)際值胶台,提交失敗。

5.線程1重新獲取內(nèi)存地址的當(dāng)前值杂抽,并重新計(jì)算想要修改的新值B诈唬。此時(shí)對線程1來說,A=11,B=12缩麸。這個(gè)重新嘗試的過程被稱為自旋铸磅。

6.這次比較幸運(yùn),沒有其他線程改變V中的值匙睹,線程1進(jìn)行Compare愚屁,發(fā)現(xiàn)A與V中的值相等。

7.線程1進(jìn)行SWAP,把地址V中的值替換為B痕檬,即12。

從思想上來說:

  • Synchronized屬于悲觀鎖送浊,悲觀地認(rèn)為程序中的并發(fā)情況嚴(yán)重梦谜,所以嚴(yán)防死守。
  • CAS屬于樂觀鎖,樂觀地認(rèn)為程序中的并發(fā)情況不那么嚴(yán)重唁桩,所以讓線程不斷去嘗試更新闭树。

使用場景

  • Synchronized同步鎖更適合在并發(fā)量非常高的情況下。
  • CAS機(jī)制用于如Atomic系列類荒澡,Lock系列類的底層實(shí)現(xiàn)报辱;Java1.6以上版本,Synchronized轉(zhuǎn)變?yōu)橹亓考?jí)鎖之前也會(huì)采用单山。

CAS缺點(diǎn)

1.CPU開銷較大

在并發(fā)量比較高的情況下碍现,如果許多線程反復(fù)嘗試更新某個(gè)變量,卻一直不成功米奸,循環(huán)往復(fù)昼接,會(huì)給CPU帶來很大的壓力。

2.不能保證代碼塊的原子性

CAS機(jī)制所保證的只是一個(gè)變量的原子性操作悴晰,而不能保證整個(gè)代碼塊的原子性慢睡。

比如需要保證3個(gè)變量共同進(jìn)行原子性的更新,就不得不使用Synchronized了铡溪。

3.ABA問題

當(dāng)一個(gè)值從A更新成B漂辐,又更新會(huì)A,普通CAS機(jī)制會(huì)誤判通過檢測棕硫。

例:

假設(shè)內(nèi)存中有個(gè)值為100的變量者吁,存儲(chǔ)在地址V中。

此時(shí)有3個(gè)線程想使用CAS的方式更新這個(gè)值饲帅,每個(gè)線程執(zhí)行的時(shí)間有略微的偏差复凳。線程1和線程2已經(jīng)獲得當(dāng)前值,線程3還未獲得當(dāng)前值灶泵。即:

  • 線程1:獲取當(dāng)前值100育八,期望更新為50;
  • 線程2:獲取當(dāng)前值100赦邻,期望更新為50髓棋;
  • 線程3:期望更新為100;

之后惶洲,線程1先執(zhí)行成功按声,把100更新為50,線程2因?yàn)槟承┰虮蛔枞衤溃醋龈虑┰颍痪€程3在1更新后,獲得當(dāng)前值50.

  • 線程1:獲取當(dāng)前值100铐料,成功更新為50渐裂;
  • 線程2:獲取當(dāng)前值100豺旬,期望更新為50,BLOCK柒凉;
  • 線程3:獲取當(dāng)前值50族阅,期望更新為100;

此時(shí)膝捞,線程2仍處于阻塞狀態(tài)坦刀,線程3繼續(xù)執(zhí)行,把50更新為100蔬咬。

  • 線程1:獲取當(dāng)前值100鲤遥,成功更新為50,已返回计盒;
  • 線程2:獲取當(dāng)前值100渴频,期望更新為50,BLOCK北启;
  • 線程3:獲取當(dāng)前值50卜朗,成功更新為100;

最后咕村,線程2終于恢復(fù)運(yùn)行狀態(tài)场钉,由于阻塞之前已經(jīng)獲得“當(dāng)前值”100,并經(jīng)過compare檢測懈涛,內(nèi)存地址V的實(shí)際值也是100逛万,所以成功把變量100更新為50.

  • 線程1:獲取當(dāng)前值100,成功更新為50批钠,已返回宇植;
  • 線程2:獲取當(dāng)前值100,成功更新為50埋心;
  • 線程3:獲取當(dāng)前值50指郁,成功更新為100,已返回拷呆;

這個(gè)過程中闲坎,線程2獲取到的變量100是一個(gè)舊值,盡管和當(dāng)前的實(shí)際值相同茬斧,但內(nèi)存地址V中的變量已經(jīng)經(jīng)歷了100->50->100(即A->B->A)的改變腰懂。

解決方案

利用版本號(hào)比較可以有效解決ABA問題。即项秉,在Compare階段不僅要比較期望值A(chǔ)和地址V中的實(shí)際值绣溜,還要比較變量的版本號(hào)是否一致。

在Java當(dāng)中伙狐,Java并發(fā)包中提供了一個(gè)帶有標(biāo)記的原子引用類AtomicStampedReference涮毫,其可以通過控制變量值的版本來保證CAS的正確性瞬欧。

Java當(dāng)中CAS的底層實(shí)現(xiàn)

下面以AtomicInteger的實(shí)現(xiàn)為例分析贷屎。

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;
    
    .....
    
    /**
     * Gets the current value.
     *
     * @return the current value
     */
    public final int get() {
        return value;
    }
    //省略部分代碼
    ......

}
  • 1.Unsafe罢防,是CAS的核心類,由于Java方法無法直接訪問底層系統(tǒng)唉侄,需要通過本地(native)方法來訪問咒吐,Unsafe相當(dāng)于一個(gè)后門,基于該類可以直接操作特定內(nèi)存的數(shù)據(jù)属划。unsafe為我們提供了硬件級(jí)別的原子操作恬叹。
  • 2.變量valueOffset,表示該變量值在內(nèi)存中的偏移地址同眯,因?yàn)閁nsafe就是根據(jù)內(nèi)存偏移地址獲取數(shù)據(jù)的绽昼。我們可以簡單地把valueOffset理解為value變量的內(nèi)存地址。
  • 3.變量value用volatile修飾须蜗,保證了多線程之間的內(nèi)存可見性硅确。即可當(dāng)做V中的值

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

    public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }
    // 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;
    }
    public native int getIntVolatile(Object var1, long var2);

線程通過getIntVolatile(var1, var2)拿到value值,compareAndSwapInt比較并替換明肮,若不相等繼續(xù)執(zhí)行g(shù)etIntVolatile菱农,然后再compareAndSwapInt,直到成功。

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

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

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

先想辦法拿到變量value在內(nèi)存中的地址。

通過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;
}
  • __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手冊對lock前綴的說明如下:

  • 1.確保后續(xù)指令執(zhí)行的原子性狠毯。

在Pentium及之前的處理器中护糖,帶有l(wèi)ock前綴的指令在執(zhí)行期間會(huì)鎖住總線,使得其它處理器暫時(shí)無法通過總線訪問內(nèi)存嚼松,很顯然嫡良,這個(gè)開銷很大锰扶。在新的處理器中,Intel使用緩存鎖定來保證指令執(zhí)行的原子性寝受,緩存鎖定將大大降低lock前綴指令的執(zhí)行開銷坷牛。

  • 2.禁止該指令與前面和后面的讀寫指令重排序。
  • 3.把寫緩沖區(qū)的所有數(shù)據(jù)刷新到內(nèi)存中很澄。

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

擴(kuò)展知識(shí)-關(guān)于CPU的鎖

關(guān)于CPU的鎖有如下3種:

1.處理器自動(dòng)保證基本內(nèi)存操作的原子性

首先處理器會(huì)自動(dòng)保證基本的內(nèi)存操作的原子性甩苛。

處理器保證從系統(tǒng)內(nèi)存當(dāng)中讀取或者寫入一個(gè)字節(jié)是原子的蹂楣,意思是當(dāng)一個(gè)處理器讀取一個(gè)字節(jié)時(shí),其他處理器不能訪問這個(gè)字節(jié)的內(nèi)存地址讯蒲。

奔騰6和最新的處理器能自動(dòng)保證單處理器對同一個(gè)緩存行里進(jìn)行16/32/64位的操作是原子的痊土,但是復(fù)雜的內(nèi)存操作處理器不能自動(dòng)保證其原子性,比如跨總線寬度墨林,跨多個(gè)緩存行赁酝,跨頁表的訪問。

但是處理器提供總線鎖定緩存鎖定兩個(gè)機(jī)制來保證復(fù)雜內(nèi)存操作的原子性萌丈。

2.使用總線鎖保證原子性

第一個(gè)機(jī)制是通過總線鎖保證原子性赞哗。

如果多個(gè)處理器同時(shí)對共享變量進(jìn)行讀改寫(i++就是經(jīng)典的讀改寫操作)操作,那么共享變量就會(huì)被多個(gè)處理器同時(shí)進(jìn)行操作辆雾,這樣讀改寫操作就不是原子的肪笋,操作完之后共享變量的值會(huì)和期望的不一致,舉個(gè)例子:如果i=1,我們進(jìn)行兩次i++操作度迂,我們期望的結(jié)果是3藤乙,但是有可能結(jié)果是2。

原因是有可能多個(gè)處理器同時(shí)從各自的緩存中讀取變量i惭墓,分別進(jìn)行加一操作坛梁,然后分別寫入系統(tǒng)內(nèi)存當(dāng)中。那么想要保證讀改寫共享變量的操作是原子的腊凶,就必須保證CPU1讀改寫共享變量的時(shí)候划咐,CPU2不能操作緩存了該共享變量內(nèi)存地址的緩存。

處理器使用總線鎖就是來解決這個(gè)問題的钧萍。所謂總線鎖就是使用處理器提供的一個(gè)LOCK#信號(hào)褐缠,當(dāng)一個(gè)處理器在總線上輸出此信號(hào)時(shí),其他處理器的請求將被阻塞住,那么該處理器可以獨(dú)占使用共享內(nèi)存风瘦。

3.使用緩存鎖保證原子性

第二個(gè)機(jī)制是通過緩存鎖定保證原子性队魏。

在同一時(shí)刻我們只需保證對某個(gè)內(nèi)存地址的操作是原子性即可,但總線鎖定把CPU和內(nèi)存之間通信鎖住了万搔,這使得鎖定期間胡桨,其他處理器不能操作其他內(nèi)存地址的數(shù)據(jù)官帘,所以總線鎖定的開銷比較大,最近的處理器在某些場合下使用緩存鎖定代替總線鎖定來進(jìn)行優(yōu)化昧谊。

頻繁使用的內(nèi)存會(huì)緩存在處理器的L1刽虹,L2和L3高速緩存里,那么原子操作就可以直接在處理器內(nèi)部緩存中進(jìn)行揽浙,并不需要聲明總線鎖状婶,在奔騰6和最近的處理器中可以使用“緩存鎖定”的方式來實(shí)現(xiàn)復(fù)雜的原子性意敛。所謂“緩存鎖定”就是如果緩存在處理器緩存行中內(nèi)存區(qū)域在LOCK操作期間被鎖定馅巷,當(dāng)它執(zhí)行鎖操作回寫內(nèi)存時(shí),處理器不在總線上聲言LOCK#信號(hào)草姻,而是修改內(nèi)部的內(nèi)存地址钓猬,并允許它的緩存一致性機(jī)制來保證操作的原子性,因?yàn)榫彺嬉恢滦詸C(jī)制會(huì)阻止同時(shí)修改被兩個(gè)以上處理器緩存的內(nèi)存區(qū)域數(shù)據(jù)撩独,當(dāng)其他處理器回寫已被鎖定的緩存行的數(shù)據(jù)時(shí)會(huì)起緩存行無效敞曹,在例1中,當(dāng)CPU1修改緩存行中的i時(shí)使用緩存鎖定综膀,那么CPU2就不能同時(shí)緩存了i的緩存行澳迫。

但是有兩種情況下處理器不會(huì)使用緩存鎖定。第一種情況是:當(dāng)操作的數(shù)據(jù)不能被緩存在處理器內(nèi)部剧劝,或操作的數(shù)據(jù)跨多個(gè)緩存行(cache line)橄登,則處理器會(huì)調(diào)用總線鎖定。第二種情況是:有些處理器不支持緩存鎖定讥此。對于Inter486和奔騰處理器,就算鎖定的內(nèi)存區(qū)域在處理器的緩存行中也會(huì)調(diào)用總線鎖定拢锹。

以上兩個(gè)機(jī)制我們可以通過Inter處理器提供了很多LOCK前綴的指令來實(shí)現(xiàn)。比如位測試和修改指令BTS萄喳,BTR卒稳,BTC,交換指令XADD他巨,CMPXCHG和其他一些操作數(shù)和邏輯指令充坑,比如ADD(加),OR(或)等染突,被這些指令操作的內(nèi)存區(qū)域就會(huì)加鎖捻爷,導(dǎo)致其他處理器不能同時(shí)訪問它。

參考資料

漫畫:什么是CAS機(jī)制觉痛?(進(jìn)階篇)

最開始了解CAS機(jī)制來源于看小灰的公眾號(hào)(chengxuyuanxiaohui)役衡,里面一共是兩篇文章,這里僅放出一篇地址薪棒,有興趣可以去公眾號(hào)查看手蝎。

深入淺出CAS

小灰里面對AtomicInteger的分析部分比較簡單而且看代碼可能略微舊些榕莺,于是找到了占小狼的和下面一篇。這主要參考了對AtomicInteger的CAS底層實(shí)現(xiàn)分析以及深到匯編的部分棵介。

JAVA CAS原理深度分析

這里也存在一些匯編钉鸯,由于和占的部分重疊就沒再多看,里面有個(gè)C++文件地址所在目錄邮辽,雖然是openjdk7的唠雕,但可以參考。主要留存了下里面的備注知識(shí)-關(guān)于CPU的鎖吨述。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末岩睁,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子揣云,更是在濱河造成了極大的恐慌捕儒,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,544評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件邓夕,死亡現(xiàn)場離奇詭異刘莹,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)焚刚,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,430評論 3 392
  • 文/潘曉璐 我一進(jìn)店門点弯,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人矿咕,你說我怎么就攤上這事抢肛。” “怎么了痴腌?”我有些...
    開封第一講書人閱讀 162,764評論 0 353
  • 文/不壞的土叔 我叫張陵雌团,是天一觀的道長。 經(jīng)常有香客問我士聪,道長锦援,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,193評論 1 292
  • 正文 為了忘掉前任剥悟,我火速辦了婚禮灵寺,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘区岗。我一直安慰自己略板,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,216評論 6 388
  • 文/花漫 我一把揭開白布慈缔。 她就那樣靜靜地躺著叮称,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上瓤檐,一...
    開封第一講書人閱讀 51,182評論 1 299
  • 那天赂韵,我揣著相機(jī)與錄音,去河邊找鬼挠蛉。 笑死楷怒,一個(gè)胖子當(dāng)著我的面吹牛救赐,可吹牛的內(nèi)容都是我干的帚呼。 我是一名探鬼主播认罩,決...
    沈念sama閱讀 40,063評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼掰担!你這毒婦竟也來了汇陆?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,917評論 0 274
  • 序言:老撾萬榮一對情侶失蹤恩敌,失蹤者是張志新(化名)和其女友劉穎瞬测,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體纠炮,經(jīng)...
    沈念sama閱讀 45,329評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,543評論 2 332
  • 正文 我和宋清朗相戀三年灯蝴,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了恢口。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,722評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡穷躁,死狀恐怖耕肩,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情问潭,我是刑警寧澤猿诸,帶...
    沈念sama閱讀 35,425評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站狡忙,受9級(jí)特大地震影響梳虽,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜灾茁,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,019評論 3 326
  • 文/蒙蒙 一窜觉、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧北专,春花似錦禀挫、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,671評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春砰左,著一層夾襖步出監(jiān)牢的瞬間画拾,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,825評論 1 269
  • 我被黑心中介騙來泰國打工菜职, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留青抛,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,729評論 2 368
  • 正文 我出身青樓酬核,卻偏偏與公主長得像蜜另,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子嫡意,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,614評論 2 353

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