CAS(Compare And Swap)比較與交換:一種無(wú)鎖算法荔睹。在不使用鎖(沒(méi)有線程被阻塞)的情況下實(shí)現(xiàn)多線程之間的變量同步浆熔。java.util.concurrent包中的原子類就是通過(guò)CAS來(lái)實(shí)現(xiàn)了樂(lè)觀鎖组哩。
CAS算法涉及到三個(gè)操作數(shù):1特漩,需要讀寫的內(nèi)存值V纤怒;2,舊的預(yù)期值A(chǔ);3克滴,要寫入的新值B逼争。
當(dāng)且僅當(dāng)V的值等于A時(shí),CAS通過(guò)原子方式用新值B來(lái)更新V的值(“比較+更新”整體是一個(gè)原子操作)劝赔,否則不會(huì)執(zhí)行任何操作誓焦。一般情況下,“更新”是一個(gè)不斷重試的操作着帽。
通過(guò)查看AtomicInteger源碼來(lái)看看具體的CAS算法杂伟。
unsafe:獲取并操作內(nèi)存的數(shù)據(jù)。
valueOffset:存儲(chǔ)value在AtomicInteger中的偏移量仍翰。
value:存儲(chǔ)AtomicInteger的int值赫粥,該屬性借助volatile關(guān)鍵字保證其在線程間是可見(jiàn)的。
看看AtomicInteger中自增方法incrementAndGet()
compareAndSwapInt中使用CAS來(lái)更新值歉备,如果更新失敗就重新獲取內(nèi)存中的值傅是,再次調(diào)用compareAndSwapInt方法更新值,直到更新成功蕾羊。在getAndAddInt方法中采用了自旋方式等待鎖釋放。
CAS通過(guò)調(diào)用JNI(Java native interface:Java本地調(diào)用)的代碼實(shí)現(xiàn)帽驯,允許Java代碼調(diào)用其他語(yǔ)言龟再。compareAndSwapInt()是本地方法調(diào)用CPU底層指令來(lái)實(shí)現(xiàn)(通過(guò)CPU的cmpxchg指令:作用是將指定內(nèi)存地址的內(nèi)容與所給的某個(gè)值相比,如果相等尼变,則將其內(nèi)容替換為指令中提供的新值利凑,如果不相等,則更新失斚邮酢)哀澈。
其實(shí)要保持?jǐn)?shù)據(jù)的一致性,都需要加鎖度气,唯一的區(qū)別就是在哪里加鎖割按,加什么鎖。本地方法調(diào)用CPU底層來(lái)實(shí)現(xiàn)的CAS磷籍,最終也是通過(guò)加鎖來(lái)完成的适荣,這里的加鎖就是在CPU上面了。
CPU的鎖:
1院领,處理器自動(dòng)保證基本內(nèi)存操作的原子性:當(dāng)一個(gè)處理器讀取一個(gè)字節(jié)時(shí)弛矛,其他處理器不能訪問(wèn)這個(gè)字節(jié)的內(nèi)存地址。這個(gè)自動(dòng)保證是在單處理器對(duì)同一個(gè)緩存行里進(jìn)行的操作是原子的比然,但是復(fù)雜的內(nèi)存操作處理不能自動(dòng)保證其原子性(跨總線寬度丈氓,跨多個(gè)緩存行,跨頁(yè)表的訪問(wèn))。在這種情況下處理器提供總線鎖定和緩存鎖定來(lái)保證復(fù)雜內(nèi)存操作的原子性万俗。
2湾笛,總線鎖保證其原子性:如果多個(gè)處理器同時(shí)對(duì)共享變量進(jìn)行讀改寫操作。那么共享變量就會(huì)被多個(gè)處理器同時(shí)進(jìn)行操作该编,這樣讀改寫操作就不是原子的迄本,操作完之后共享變量的值會(huì)和期望的不一致。例如i++操作课竣,多個(gè)處理器同時(shí)從各自的緩存中讀取變量i嘉赎,分別進(jìn)行加一操作,然后分別寫入系統(tǒng)內(nèi)存當(dāng)中于樟。那么想要保證讀改寫共享變量的操作是原子的公条,就必須保證一個(gè)CPU讀改寫共享變量的時(shí)候,其他CPU不能操作緩存了該共享變量?jī)?nèi)存地址的緩存迂曲。
總線鎖可以解決這個(gè)問(wèn)題靶橱,總線鎖就是使用處理器提供的一個(gè)LOCK#信號(hào),當(dāng)一個(gè)處理器在總線上輸出此信號(hào)時(shí)路捧,其他處理器的請(qǐng)求將被阻塞住,那么該處理器可以獨(dú)占使用共享內(nèi)存(其他處理器暫時(shí)無(wú)法通過(guò)總線訪問(wèn)內(nèi)存)关霸。總線鎖把CPU和內(nèi)存之間的通信鎖住了杰扫,這使得鎖定期間队寇,其他處理器不能操作其他內(nèi)存地址的數(shù)據(jù),這個(gè)開(kāi)銷比較大章姓,往往也不需要佳遣。其實(shí)大多數(shù)的時(shí)候只需要鎖定和處理數(shù)據(jù)相關(guān)的緩存的內(nèi)存地址就行了。
3凡伊,緩存鎖保證原子性:在同一時(shí)刻我們只需保證對(duì)某個(gè)內(nèi)存地址的操作是原子性零渐,所以出現(xiàn)了緩存鎖。頻繁使用的內(nèi)存會(huì)緩存在處理器高速緩存里系忙。那么原子操作就可以直接在處理器內(nèi)部緩存中進(jìn)行诵盼,并不需要進(jìn)行總線鎖。緩存鎖定:如果緩存在處理器緩存行中的內(nèi)存區(qū)域在LOCK操作期間被鎖定笨觅,當(dāng)它執(zhí)行鎖操作回寫內(nèi)存時(shí)拦耐,處理器不在總線上聲明LOCK#信號(hào),而是修改內(nèi)部的內(nèi)存地址见剩。并允許它的緩存一致性機(jī)制來(lái)保證操作的原子性杀糯,因?yàn)榫彺嬉恢滦詸C(jī)制會(huì)阻止同時(shí)修改被兩個(gè)以上處理器緩存的內(nèi)存區(qū)域數(shù)據(jù),當(dāng)其他處理器回寫已被鎖定的緩存行的數(shù)據(jù)時(shí)會(huì)起緩存行無(wú)效(緩存一致性機(jī)制:是當(dāng)某塊CPU對(duì)緩存中的數(shù)據(jù)進(jìn)行操作了之后苍苞,就通知其他CPU放棄儲(chǔ)存在它們內(nèi)部的緩存固翰,或者從主內(nèi)存中重新讀壤俏场)。參考:MESI協(xié)議
例外:在處理器支持緩存鎖的情況下骂际,以下情況處理器不會(huì)使用緩存鎖定:1疗琉,當(dāng)操作的數(shù)據(jù)不能被緩存在處理器內(nèi)部或者操作的數(shù)據(jù)跨多個(gè)緩存行,則處理器調(diào)用總線鎖歉铝。
疑問(wèn):在CPU層面的緩存一致性機(jī)制已經(jīng)保持了共享變量的一致性問(wèn)題盈简,那么在代碼層面上的多線程代碼為什么還需要同步?
剛剛網(wǎng)上看到了一種說(shuō)法:有了緩存一致性協(xié)議為什么還需要多線程同步太示? - 知乎
CAS的缺點(diǎn):
1柠贤,ABA問(wèn)題:因?yàn)镃AS需要在操作值的時(shí)候檢查下值有沒(méi)有發(fā)生變化,如果沒(méi)有發(fā)生變化則更新类缤,但是如果一個(gè)值原來(lái)是A臼勉,變成了B,又變成了A餐弱,那么使用CAS進(jìn)行檢查時(shí)會(huì)發(fā)現(xiàn)它的值沒(méi)有發(fā)生變化宴霸,但是實(shí)際上卻變化了。ABA問(wèn)題的解決思路就是使用版本號(hào)膏蚓。在變量前面追加上版本號(hào)瓢谢,每次變量更新的時(shí)候把版本號(hào)加一,那么A-B-A 就會(huì)變成1A-2B-3A驮瞧。
2恩闻,循環(huán)時(shí)間長(zhǎng)開(kāi)銷大:自旋CAS如果長(zhǎng)時(shí)間不成功,會(huì)給CPU帶來(lái)非常大的執(zhí)行開(kāi)銷剧董。
3,只能保證一個(gè)共享變量的原子操作:當(dāng)對(duì)一個(gè)共享變量執(zhí)行操作時(shí)破停,我們可以使用循環(huán)CAS的方式來(lái)保證原子操作翅楼,但是對(duì)多個(gè)共享變量操作時(shí),循環(huán)CAS就無(wú)法保證操作的原子性真慢,這個(gè)時(shí)候就可以用鎖毅臊,或者有一個(gè)取巧的辦法,就是把多個(gè)共享變量合并成一個(gè)共享變量來(lái)操作黑界。比如有兩個(gè)共享變量i=2,j=a管嬉,合并一下ij=2a,然后用CAS來(lái)操作ij朗鸠。從Java1.5開(kāi)始JDK提供了AtomicReference類來(lái)保證引用對(duì)象之間的原子性蚯撩,你可以把多個(gè)變量放在一個(gè)對(duì)象里來(lái)進(jìn)行CAS操作。
參考: