6、CAS基本原理



1、什么是原子操作?如何實(shí)現(xiàn)原子操作?

假定有兩個(gè)操作A和B(A和B可能都很復(fù)雜),如果從執(zhí)行A的線程來看,當(dāng)另一個(gè)線程執(zhí)行B時(shí),要么將B全部執(zhí)行完,要么完全不執(zhí)行B,那么A和B對彼此來說是原子的。

實(shí)現(xiàn)原子操作可以使用鎖,鎖機(jī)制,滿足基本的需求是沒有問題的了,但是有的時(shí)候我們的需求并非這么簡單,我們需要更有效,更加靈活的機(jī)制,synchronized關(guān)鍵字是基于阻塞的鎖機(jī)制,也就是說當(dāng)一個(gè)線程擁有鎖的時(shí)候,訪問同一資源的其它線程需要等待,直到該線程釋放鎖,

這里會(huì)有些問題:首先,如果被阻塞的線程優(yōu)先級很高很重要怎么辦?其次,如果獲得鎖的線程一直不釋放鎖怎么辦?(這種情況是非常糟糕的)。還有一種情況,如果有大量的線程來競爭資源,那CPU將會(huì)花費(fèi)大量的時(shí)間和資源來處理這些競爭,同時(shí),還有可能出現(xiàn)一些例如死鎖之類的情況,最后,其實(shí)鎖機(jī)制是一種比較粗糙,粒度比較大的機(jī)制,相對于像計(jì)數(shù)器這樣的需求有點(diǎn)兒過于笨重。

實(shí)現(xiàn)原子操作還可以使用當(dāng)前的處理器基本都支持CAS()的指令,只不過每個(gè)廠家所實(shí)現(xiàn)的算法并不一樣,每一個(gè)CAS操作過程都包含三個(gè)運(yùn)算符:一個(gè)內(nèi)存地址V,一個(gè)期望的值A(chǔ)和一個(gè)新值B,操作的時(shí)候如果這個(gè)地址上存放的值等于這個(gè)期望的值A(chǔ),則將地址上的值賦為新值B,否則不做任何操作。

CAS的基本思路就是,如果這個(gè)地址上的值和期望的值相等,則給其賦予新值,否則不做任何事兒,但是要返回原值是多少。循環(huán)CAS就是在一個(gè)循環(huán)里不斷的做cas操作,直到成功為止。


6.1.1 CAS 操作

2、CAS實(shí)現(xiàn)原子操作的三大問題

ABA問題:

因?yàn)镃AS需要在操作值的時(shí)候,檢查值有沒有發(fā)生變化,如果沒有發(fā)生變化則更新,但是如果一個(gè)值原來是A,變成了B,又變成了A,那么使用CAS進(jìn)行檢查時(shí)會(huì)發(fā)現(xiàn)它的值沒有發(fā)生變化,但是實(shí)際上卻變化了。

ABA問題的解決思路就是使用版本號。在變量前面追加上版本號,每次變量更新的時(shí)候把版本號加1,那么A→B→A就會(huì)變成1A→2B→3A。舉個(gè)通俗點(diǎn)的例子,你倒了一杯水放桌子上,干了點(diǎn)別的事,然后同事把你水喝了又給你重新倒了一杯水,你回來看水還在,拿起來就喝,如果你不管水中間被人喝過,只關(guān)心水還在,這就是ABA問題。

如果你是一個(gè)講衛(wèi)生講文明的小伙子,不但關(guān)心水在不在,還要在你離開的時(shí)候水被人動(dòng)過沒有,因?yàn)槟闶浅绦騿T,所以就想起了放了張紙?jiān)谂赃叄瑢懮铣跏贾?,別人喝水前麻煩先做個(gè)累加才能喝水。

循環(huán)時(shí)間長開銷大:

自旋CAS如果長時(shí)間不成功,會(huì)給CPU帶來非常大的執(zhí)行開銷。

只能保證一個(gè)共享變量的原子操作:

當(dāng)對一個(gè)共享變量執(zhí)行操作時(shí),我們可以使用循環(huán)CAS的方式來保證原子操作,但是對多個(gè)共享變量操作時(shí),循環(huán)CAS就無法保證操作的原子性,這個(gè)時(shí)候就可以用鎖。

還有一個(gè)取巧的辦法,就是把多個(gè)共享變量合并成一個(gè)共享變量來操作。比如,有兩個(gè)共享變量i=2,j=a,合并一下ij=2a,然后用CAS來操作ij。從Java 1.5開始,JDK提供了AtomicReference類來保證引用對象之間的原子性,就可以把多個(gè)變量放在一個(gè)對象里來進(jìn)行CAS操作。

3、Jdk中相關(guān)原子操作類的使用

AtomicInteger:

?int addAndGet(int delta):以原子方式將輸入的數(shù)值與實(shí)例中的值(AtomicInteger里的value)相加,并返回結(jié)果。

?boolean compareAndSet(int expect,int update):如果輸入的數(shù)值等于預(yù)期值,則以原子方式將該值設(shè)置為輸入的值。

?int getAndIncrement():以原子方式將當(dāng)前值加1,注意,這里返回的是自增前的值。

?int getAndSet(int newValue):以原子方式設(shè)置為newValue的值,并返回舊值。

AtomicIntegerArray:

主要是提供原子的方式更新數(shù)組里的整型,其常用方法如下。

?int addAndGet(int i,int delta):以原子方式將輸入值與數(shù)組中索引i的元素相加。

?boolean compareAndSet(int i,int expect,int update):如果當(dāng)前值等于預(yù)期值,則以原子方式將數(shù)組位置i的元素設(shè)置成update值。

需要注意的是,數(shù)組value通過構(gòu)造方法傳遞進(jìn)去,然后AtomicIntegerArray會(huì)將當(dāng)前數(shù)組復(fù)制一份,所以當(dāng)AtomicIntegerArray對內(nèi)部的數(shù)組元素進(jìn)行修改時(shí),不會(huì)影響傳入的數(shù)組。

更新引用類型

原子更新基本類型的AtomicInteger,只能更新一個(gè)變量,如果要原子更新多個(gè)變量,就需要使用這個(gè)原子更新引用類型提供的類。Atomic包提供了以下3個(gè)類。

AtomicReference:

原子更新引用類型。

AtomicStampedReference:

利用版本戳的形式記錄了每次改變以后的版本號,這樣的話就不會(huì)存在ABA問題了。這就是AtomicStampedReference的解決方案。AtomicMarkableReference跟AtomicStampedReference差不多, AtomicStampedReference是使用pair的int stamp作為計(jì)數(shù)器使用,AtomicMarkableReference的pair使用的是boolean mark。 還是那個(gè)水的例子,AtomicStampedReference可能關(guān)心的是動(dòng)過幾次,AtomicMarkableReference關(guān)心的是有沒有被人動(dòng)過,方法都比較簡單。

AtomicMarkableReference:

原子更新帶有標(biāo)記位的引用類型。可以原子更新一個(gè)布爾類型的標(biāo)記位和引用類型。構(gòu)造方法是AtomicMarkableReference(V initialRef,booleaninitialMark)。

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

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

  • 什么是原子操作?如何實(shí)現(xiàn)原子操作? 假定有兩個(gè)操作A和B(A和B可能都很復(fù)雜),如果從執(zhí)行A的線程來看,當(dāng)另一個(gè)線...
    dashu52閱讀 438評論 0 0
  • 一、什么是原子操作?如何實(shí)現(xiàn)原子操作? 如果有兩個(gè)操作A和B,分別有兩個(gè)不同的線程執(zhí)行。從A所在線程來看,執(zhí)行B的...
    大蝦啊啊啊閱讀 465評論 0 0
  • CAS? 比較并交換(compare and swap, CAS),是原子操作的一種,可用于在多線程編程中實(shí)現(xiàn)不被...
    仕明同學(xué)閱讀 1,765評論 0 1
  • 并發(fā)基礎(chǔ)知識補(bǔ)全 Callable、Future和FutureTask 在前文(線程基礎(chǔ)、線程之間的共享與協(xié)作)中...
    暮暮頻顧惜閱讀 302評論 0 0
  • 什么是原子性 原子不可分割(在還未發(fā)現(xiàn)原子核與電子時(shí))。假設(shè)有兩個(gè)線程,從一個(gè)線程看,另一個(gè)線程要么全部執(zhí)行完,要...
    NamelessPeople閱讀 1,043評論 0 0

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