溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶(hù)服務(wù)條款》

并發(fā)編程的靈魂:CAS機(jī)制詳解

發(fā)布時(shí)間:2020-04-05 07:43:33 來(lái)源:網(wǎng)絡(luò) 閱讀:365 作者:Java_老男孩 欄目:編程語(yǔ)言

Java中提供了很多原子操作類(lèi)來(lái)保證共享變量操作的原子性。這些原子操作的底層原理都是使用了CAS機(jī)制。在使用一門(mén)技術(shù)之前,了解這個(gè)技術(shù)的底層原理是非常重要的,所以本篇文章就先來(lái)講講什么是CAS機(jī)制,CAS機(jī)制存在的一些問(wèn)題以及在Java中怎么使用CAS機(jī)制。

其實(shí)Java并發(fā)框架的基石一共有兩塊,一塊是本文介紹的CAS,另一塊就是AQS,后續(xù)也會(huì)寫(xiě)文章介紹。

什么是CAS機(jī)制

CAS機(jī)制是一種數(shù)據(jù)更新的方式。在具體講什么是CAS機(jī)制之前,我們先來(lái)聊下在多線(xiàn)程環(huán)境下,對(duì)共享變量進(jìn)行數(shù)據(jù)更新的兩種模式:悲觀鎖模式和樂(lè)觀鎖模式。

悲觀鎖更新的方式認(rèn)為:在更新數(shù)據(jù)的時(shí)候大概率會(huì)有其他線(xiàn)程去爭(zhēng)奪共享資源,所以悲觀鎖的做法是:第一個(gè)獲取資源的線(xiàn)程會(huì)將資源鎖定起來(lái),其他沒(méi)爭(zhēng)奪到資源的線(xiàn)程只能進(jìn)入阻塞隊(duì)列,等第一個(gè)獲取資源的線(xiàn)程釋放鎖之后,這些線(xiàn)程才能有機(jī)會(huì)重新?tīng)?zhēng)奪資源。synchronized就是java中悲觀鎖的典型實(shí)現(xiàn),synchronized使用起來(lái)非常簡(jiǎn)單方便,但是會(huì)使沒(méi)爭(zhēng)搶到資源的線(xiàn)程進(jìn)入阻塞狀態(tài),線(xiàn)程在阻塞狀態(tài)和Runnable狀態(tài)之間切換效率較低(比較慢)。比如你的更新操作其實(shí)是非常快的,這種情況下你還用synchronized將其他線(xiàn)程都鎖住了,線(xiàn)程從Blocked狀態(tài)切換回Runnable華的時(shí)間可能比你的更新操作的時(shí)間還要長(zhǎng)。

樂(lè)觀鎖更新方式認(rèn)為:在更新數(shù)據(jù)的時(shí)候其他線(xiàn)程爭(zhēng)搶這個(gè)共享變量的概率非常小,所以更新數(shù)據(jù)的時(shí)候不會(huì)對(duì)共享數(shù)據(jù)加鎖。但是在正式更新數(shù)據(jù)之前會(huì)檢查數(shù)據(jù)是否被其他線(xiàn)程改變過(guò),如果未被其他線(xiàn)程改變過(guò)就將共享變量更新成最新值,如果發(fā)現(xiàn)共享變量已經(jīng)被其他線(xiàn)程更新過(guò)了,就重試,直到成功為止。CAS機(jī)制就是樂(lè)觀鎖的典型實(shí)現(xiàn)。

CAS,是Compare and Swap的簡(jiǎn)稱(chēng),在這個(gè)機(jī)制中有三個(gè)核心的參數(shù):

  • 主內(nèi)存中存放的共享變量的值:V(一般情況下這個(gè)V是內(nèi)存的地址值,通過(guò)這個(gè)地址可以獲得內(nèi)存中的值)
  • 工作內(nèi)存中共享變量的副本值,也叫預(yù)期值:A
  • 需要將共享變量更新到的最新值:B

并發(fā)編程的靈魂:CAS機(jī)制詳解

如上圖中,主存中保存V值,線(xiàn)程中要使用V值要先從主存中讀取V值到線(xiàn)程的工作內(nèi)存A中,然后計(jì)算后變成B值,最后再把B值寫(xiě)回到內(nèi)存V值中。多個(gè)線(xiàn)程共用V值都是如此操作。CAS的核心是在將B值寫(xiě)入到V之前要比較A值和V值是否相同,如果不相同證明此時(shí)V值已經(jīng)被其他線(xiàn)程改變,重新將V值賦給A,并重新計(jì)算得到B,如果相同,則將B值賦給V。

值得注意的是CAS機(jī)制中的這步步驟是原子性的(從指令層面提供的原子操作),所以CAS機(jī)制可以解決多線(xiàn)程并發(fā)編程對(duì)共享變量讀寫(xiě)的原子性問(wèn)題。

CAS機(jī)制優(yōu)缺點(diǎn)

缺點(diǎn)

1. ABA問(wèn)題
ABA問(wèn)題:CAS在操作的時(shí)候會(huì)檢查變量的值是否被更改過(guò),如果沒(méi)有則更新值,但是帶來(lái)一個(gè)問(wèn)題,最開(kāi)始的值是A,接著變成B,最后又變成了A。經(jīng)過(guò)檢查這個(gè)值確實(shí)沒(méi)有修改過(guò),因?yàn)樽詈蟮闹颠€是A,但是實(shí)際上這個(gè)值確實(shí)已經(jīng)被修改過(guò)了。為了解決這個(gè)問(wèn)題,在每次進(jìn)行操作的時(shí)候加上一個(gè)版本號(hào),每次操作的就是兩個(gè)值,一個(gè)版本號(hào)和某個(gè)值,A——>B——>A問(wèn)題就變成了1A——>2B——>3A。在jdk中提供了AtomicStampedReference類(lèi)解決ABA問(wèn)題,用Pair這個(gè)內(nèi)部類(lèi)實(shí)現(xiàn),包含兩個(gè)屬性,分別代表版本號(hào)和引用,在compareAndSet中先對(duì)當(dāng)前引用進(jìn)行檢查,再對(duì)版本號(hào)標(biāo)志進(jìn)行檢查,只有全部相等才更新值。

2. 可能會(huì)消耗較高的CPU
看起來(lái)CAS比鎖的效率高,從阻塞機(jī)制變成了非阻塞機(jī)制,減少了線(xiàn)程之間等待的時(shí)間。每個(gè)方法不能絕對(duì)的比另一個(gè)好,在線(xiàn)程之間競(jìng)爭(zhēng)程度大的時(shí)候,如果使用CAS,每次都有很多的線(xiàn)程在競(jìng)爭(zhēng),也就是說(shuō)CAS機(jī)制不能更新成功。這種情況下CAS機(jī)制會(huì)一直重試,這樣就會(huì)比較耗費(fèi)CPU。因此可以看出,如果線(xiàn)程之間競(jìng)爭(zhēng)程度小,使用CAS是一個(gè)很好的選擇;但是如果競(jìng)爭(zhēng)很大,使用鎖可能是個(gè)更好的選擇。在并發(fā)量非常高的環(huán)境中,如果仍然想通過(guò)原子類(lèi)來(lái)更新的話(huà),可以使用AtomicLong的替代類(lèi):LongAdder。

3. 不能保證代碼塊的原子性
Java中的CAS機(jī)制只能保證共享變量操作的原子性,而不能保證代碼塊的原子性。

優(yōu)點(diǎn)

  • 可以保證變量操作的原子性;
  • 并發(fā)量不是很高的情況下,使用CAS機(jī)制比使用鎖機(jī)制效率更高;
  • 在線(xiàn)程對(duì)共享資源占用時(shí)間較短的情況下,使用CAS機(jī)制效率也會(huì)較高。

Java提供的CAS操作類(lèi)--Unsafe類(lèi)

從Java5開(kāi)始引入了對(duì)CAS機(jī)制的底層的支持,在這之前需要開(kāi)發(fā)人員編寫(xiě)相關(guān)的代碼才可以實(shí)現(xiàn)CAS。在原子變量類(lèi)Atomic中(例如AtomicInteger、AtomicLong)可以看到CAS操作的代碼,在這里的代碼都是調(diào)用了底層(核心代碼調(diào)用native修飾的方法)的實(shí)現(xiàn)方法。在AtomicInteger源碼中可以看getAndSet方法和compareAndSet方法之間的關(guān)系,compareAndSet方法調(diào)用了底層的實(shí)現(xiàn),該方法可以實(shí)現(xiàn)與一個(gè)volatile變量的讀取和寫(xiě)入相同的效果。在前面說(shuō)到了volatile不支持例如i++這樣的復(fù)合操作,在Atomic中提供了實(shí)現(xiàn)該操作的方法。JVM對(duì)CAS的支持通過(guò)這些原子類(lèi)(Atomic***)暴露出來(lái),供我們使用。

而Atomic系類(lèi)的類(lèi)底層調(diào)用的是Unsafe類(lèi)的API,Unsafe類(lèi)提供了一系列的compareAndSwap*方法,下面就簡(jiǎn)單介紹下Unsafe類(lèi)的API:

  • long objectFieldOffset(Field field)方法:返回指定的變量在所屬類(lèi)中的內(nèi)存偏移地址,該偏移地址僅僅在該Unsafe函數(shù)中訪問(wèn)指定字段時(shí)使用。如下代碼使用Unsafe類(lèi)獲取變量value在AtomicLong對(duì)象中的內(nèi)存偏移。
    static {
    try {
       valueOffset = unsafe.objectFieldOffset
           (AtomicInteger.class.getDeclaredField("value"));
    } catch (Exception ex) { throw new Error(ex); }
    }
  • int arrayBaseOffset(Class arrayClass)方法:獲取數(shù)組中第一個(gè)元素的地址。
  • int arrayIndexScale(Class arrayClass)方法:獲取數(shù)組中一個(gè)元素占用的字節(jié)。
  • boolean compareAndSwapLong(Object obj, long offset, long expect, long update)方法:比較對(duì)象obj中偏移量為offset的變量的值是否與expect相等,相等則使用update值更新,然后返回true,否則返回false。
  • public native long getLongvolatile(Object obj, long offset)方法:獲取對(duì)象obj中偏移量為offset的變量對(duì)應(yīng)volatile語(yǔ)義的值。
  • void putLongvolatile(Object obj, long offset, long value)方法:設(shè)置obj對(duì)象中offset偏移的類(lèi)型為long的field的值為value,支持volatile語(yǔ)義。
  • void putOrderedLong(Object obj, long offset, long value)方法:設(shè)置obj對(duì)象中offset偏移地址對(duì)應(yīng)的long型field的值為value。這是一個(gè)有延遲的putLongvolatile方法,并且不保證值修改對(duì)其他線(xiàn)程立刻可見(jiàn)。只有在變量使用volatile修飾并且預(yù)計(jì)會(huì)被意外修改時(shí)才使用該方法。
  • void park(boolean isAbsolute, long time)方法:阻塞當(dāng)前線(xiàn)程,其中參數(shù)isAbsolute等于false且time等于0表示一直阻塞。time大于0表示等待指定的time后阻塞線(xiàn)程會(huì)被喚醒,這個(gè)time是個(gè)相對(duì)值,是個(gè)增量值,也就是相對(duì)當(dāng)前時(shí)間累加time后當(dāng)前線(xiàn)程就會(huì)被喚醒。如果isAbsolute等于true,并且time大于0,則表示阻塞的線(xiàn)程到指定的時(shí)間點(diǎn)后會(huì)被喚醒,這里time是個(gè)絕對(duì)時(shí)間,是將某個(gè)時(shí)間點(diǎn)換算為ms后的值。另外,當(dāng)其他線(xiàn)程調(diào)用了當(dāng)前阻塞線(xiàn)程的interrupt方法而中斷了當(dāng)前線(xiàn)程時(shí),當(dāng)前線(xiàn)程也會(huì)返回,而當(dāng)其他線(xiàn)程調(diào)用了unPark方法并且把當(dāng)前線(xiàn)程作為參數(shù)時(shí)當(dāng)前線(xiàn)程也會(huì)返回。
  • void unpark(Object thread)方法:?jiǎn)拘颜{(diào)用park后阻塞的線(xiàn)程。

下面是JDK8新增的函數(shù),這里只列出Long類(lèi)型操作。

  • long getAndSetLong(Object obj, long offset, long update)方法:獲取對(duì)象obj中偏移量為offset的變量volatile語(yǔ)義的當(dāng)前值,并設(shè)置變量volatile語(yǔ)義的值為update。

    //這個(gè)方法只是封裝了compareAndSwapLong的使用,不需要自己寫(xiě)重試機(jī)制
    public final long getAndSetLong(Object var1, long var2, long var4) {
    long var6;
    do {
        var6 = this.getLongVolatile(var1, var2);
    } while(!this.compareAndSwapLong(var1, var2, var6, var4));
    
    return var6;
    }
  • long getAndAddLong(Object obj, long offset, long addValue)方法:獲取對(duì)象obj中偏移量為offset的變量volatile語(yǔ)義的當(dāng)前值,并設(shè)置變量值為原始值+addValue,原理和上面的方法類(lèi)似。

CAS使用場(chǎng)景

  • 使用一個(gè)變量統(tǒng)計(jì)網(wǎng)站的訪問(wèn)量;
  • Atomic類(lèi)操作;
  • 數(shù)據(jù)庫(kù)樂(lè)觀鎖更新。
向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI