CAS簡(jiǎn)介
CAS英文名稱(chēng)為Compare-And-Swap,中文叫做“比較并交換”,它是一種思想、一種算法。在多線(xiàn)程的情況下,各個(gè)代碼的執(zhí)行順序是不能確定的,所以為了保證并發(fā)安全,我們可以使用互斥鎖。而 CAS 的特點(diǎn)是避免使用互斥鎖,當(dāng)多個(gè)線(xiàn)程同時(shí)使用 CAS 更新同一個(gè)變量時(shí),只有其中一個(gè)線(xiàn)程能夠操作成功,而其他線(xiàn)程都會(huì)更新失敗。不過(guò)和同步互斥鎖不同的是,更新失敗的線(xiàn)程并不會(huì)被阻塞,而是被告知這次由于競(jìng)爭(zhēng)而導(dǎo)致的操作失敗,但還可以再次嘗試。
CAS 被廣泛應(yīng)用在并發(fā)編程領(lǐng)域中,以實(shí)現(xiàn)那些不會(huì)被打斷的數(shù)據(jù)交換操作,從而就實(shí)現(xiàn)了無(wú)鎖的線(xiàn)程安全。
CAS 的思路
在大多數(shù)處理器的指令中,都會(huì)實(shí)現(xiàn) CAS 相關(guān)的指令,這一條指令就可以完成“比較并交換”的操作,也正是由于這是一條(而不是多條)CPU 指令,所以 CAS 相關(guān)的指令是具備原子性的,這個(gè)組合操作在執(zhí)行期間不會(huì)被打斷,這樣就能保證并發(fā)安全。由于這個(gè)原子性是由 CPU 保證的,所以無(wú)需我們程序員來(lái)操心。
CAS 有三個(gè)操作數(shù):內(nèi)存值 V、預(yù)期值 A、要修改的值 B。CAS 最核心的思路就是,僅當(dāng)預(yù)期值 A 和當(dāng)前的內(nèi)存值 V 相同時(shí),才將內(nèi)存值修改為 B。
我們對(duì)此展開(kāi)描述一下:CAS 會(huì)提前假定當(dāng)前內(nèi)存值 V 應(yīng)該等于值 A,而值 A 往往是之前讀取到當(dāng)時(shí)的內(nèi)存值 V。在執(zhí)行 CAS 時(shí),如果發(fā)現(xiàn)當(dāng)前的內(nèi)存值 V 恰好是值 A 的話(huà),那 CAS 就會(huì)把內(nèi)存值 V 改成值 B,而值 B 往往是在拿到值 A 后,在值 A 的基礎(chǔ)上經(jīng)過(guò)計(jì)算而得到的。如果執(zhí)行 CAS 時(shí)發(fā)現(xiàn)此時(shí)內(nèi)存值 V 不等于值 A,則說(shuō)明在剛才計(jì)算 B 的期間內(nèi),內(nèi)存值已經(jīng)被其他線(xiàn)程修改過(guò)了,那么本次 CAS 就不應(yīng)該再修改了,可以避免多人同時(shí)修改導(dǎo)致出錯(cuò)。這就是 CAS 的主要思路和流程。
JDK 正是利用了這些 CAS 指令,可以實(shí)現(xiàn)并發(fā)的數(shù)據(jù)結(jié)構(gòu),比如 AtomicInteger 等原子類(lèi)。
利用 CAS 實(shí)現(xiàn)的無(wú)鎖算法,就像我們談判的時(shí)候,用一種非常樂(lè)觀的方式去協(xié)商,彼此之間很友好,這次沒(méi)談成,還可以重試。CAS 的思路和之前的互斥鎖是兩種完全不同的思路,如果是互斥鎖,不存在協(xié)商機(jī)制,大家都會(huì)嘗試搶占資源,如果搶到了,在操作完成前,會(huì)把這個(gè)資源牢牢的攥在自己的手里。當(dāng)然,利用 CAS 和利用互斥鎖,都可以保證并發(fā)安全,它們是實(shí)現(xiàn)同一目標(biāo)的不同手段。
CAS的缺點(diǎn)
ABA 問(wèn)題
首先,CAS 最大的缺點(diǎn)就是 ABA 問(wèn)題。
決定 CAS 是否進(jìn)行 swap 的判斷標(biāo)準(zhǔn)是“當(dāng)前的值和預(yù)期的值是否一致”,如果一致,就認(rèn)為在此期間這個(gè)數(shù)值沒(méi)有發(fā)生過(guò)變動(dòng),這在大多數(shù)情況下是沒(méi)有問(wèn)題的。
但是在有的業(yè)務(wù)場(chǎng)景下,我們想確切知道從上一次看到這個(gè)值以來(lái)到現(xiàn)在,這個(gè)值是否發(fā)生過(guò)變化。例如,這個(gè)值假設(shè)從 A 變成了 B,再由 B 變回了 A,此時(shí),我們不僅認(rèn)為它發(fā)生了變化,并且會(huì)認(rèn)為它變化了兩次。
在這種場(chǎng)景下,我們使用 CAS,就看不到這兩次的變化,因?yàn)閮H判斷“當(dāng)前的值和預(yù)期的值是否一致”就是不夠的了。CAS 檢查的并不是值有沒(méi)有發(fā)生過(guò)變化,而是去比較這當(dāng)前的值和預(yù)期值是不是相等,如果變量的值從舊值 A 變成了新值 B 再變回舊值 A,由于最開(kāi)始的值 A 和現(xiàn)在的值 A 是相等的,所以 CAS 會(huì)認(rèn)為變量的值在此期間沒(méi)有發(fā)生過(guò)變化。所以,CAS 并不能檢測(cè)出在此期間值是不是被修改過(guò),它只能檢查出現(xiàn)在的值和最初的值是不是一樣。
比如:
1、線(xiàn)程1要把1變成2,但還沒(méi)執(zhí)行
2、線(xiàn)程2此時(shí)已經(jīng)把1變成2
3、線(xiàn)程3又把2變回1
4、此時(shí)線(xiàn)程1執(zhí)行,發(fā)現(xiàn)等于預(yù)期值1,執(zhí)行等于2的邏輯
這就是ABA的問(wèn)題,也就是線(xiàn)程1是無(wú)法感知到是否有其他線(xiàn)程修改過(guò)這個(gè)值,CAS判斷由于符合預(yù)期值,那么就認(rèn)為沒(méi)有被修改,所以它會(huì)繼續(xù)下邊的操作。那么這個(gè)ABA有沒(méi)有影響,有兩種情況:
1、如果值是基本類(lèi)型比如1、2、3、4這種,那么就不會(huì)有影響。
2、如果值是引用對(duì)象,那么就可能會(huì)產(chǎn)生影響,因?yàn)榭赡芷渌€(xiàn)程改變了對(duì)象中的東西。打個(gè)比方:你的手機(jī)被別人拿去用了幾天,你再用的時(shí)候發(fā)現(xiàn),手機(jī)還是這個(gè)手機(jī),但手機(jī)里邊的東西可能和之前不一樣了。
那么如何解決這個(gè)問(wèn)題呢?添加一個(gè)版本號(hào)就可以解決。
在 atomic 包中提供了 AtomicStampedReference 這個(gè)類(lèi),它是專(zhuān)門(mén)用來(lái)解決 ABA 問(wèn)題的,
AtomicStampedReference 會(huì)維護(hù)一種類(lèi)似 <Object,int> 的數(shù)據(jù)結(jié)構(gòu),其中的 int 就是用于計(jì)數(shù)的,也就是版本號(hào),它可以對(duì)這個(gè)對(duì)象和 int 版本號(hào)同時(shí)進(jìn)行原子更新,從而也就解決了 ABA 問(wèn)題。因?yàn)槲覀內(nèi)ヅ袛嗨欠癖恍薷倪^(guò),不再是以值是否發(fā)生變化為標(biāo)準(zhǔn),而是以版本號(hào)是否變化為標(biāo)準(zhǔn),即使值一樣,它們的版本號(hào)也是不同的,因?yàn)樗故玖诉@個(gè)值的變化路徑
1A→2B→3A,這樣一來(lái),就可以通過(guò)對(duì)比版本號(hào)來(lái)判斷值是否變化過(guò)。
自旋時(shí)間長(zhǎng)開(kāi)銷(xiāo)大
由于單次 CAS 不一定能執(zhí)行成功,所以 CAS 往往是配合著循環(huán)來(lái)實(shí)現(xiàn)的,有的時(shí)候甚至是死循環(huán),不停地進(jìn)行重試,直到線(xiàn)程競(jìng)爭(zhēng)不激烈的時(shí)候,才能修改成功。
可是如果我們的應(yīng)用場(chǎng)景本身就是高并發(fā)的場(chǎng)景,就有可能導(dǎo)致 CAS 一直都操作不成功,這樣的話(huà),循環(huán)時(shí)間就會(huì)越來(lái)越長(zhǎng)。而且在此期間,CPU 資源也是一直在被消耗的,這會(huì)對(duì)性能產(chǎn)生很大的影響。所以這就要求我們,要根據(jù)實(shí)際情況來(lái)選擇是否使用 CAS,在高并發(fā)的場(chǎng)景下,通常 CAS 的效率是不高的。
范圍不能靈活控制
通常我們?nèi)?zhí)行 CAS 的時(shí)候,是針對(duì)某一個(gè),而不是多個(gè)共享變量的,這個(gè)變量可能是 Integer 類(lèi)型,也有可能是 Long 類(lèi)型、對(duì)象類(lèi)型等等,但是我們不能針對(duì)多個(gè)共享變量同時(shí)進(jìn)行 CAS 操作,因?yàn)檫@多個(gè)變量之間是獨(dú)立的,簡(jiǎn)單的把原子操作組合到一起,并不具備原子性。因此如果我們想對(duì)多個(gè)對(duì)象同時(shí)進(jìn)行 CAS 操作并想保證線(xiàn)程安全的話(huà),是比較困難的。
有一個(gè)解決方案,那就是利用一個(gè)新的類(lèi),來(lái)整合剛才這一組共享變量,這個(gè)新的類(lèi)中的多個(gè)成員變量就是剛才的那多個(gè)共享變量,然后再利用 atomic 包中的 AtomicReference 來(lái)把這個(gè)新對(duì)象整體進(jìn)行 CAS 操作,這樣就可以保證線(xiàn)程安全。
相比之下,如果我們使用其他的線(xiàn)程安全技術(shù),那么調(diào)整線(xiàn)程安全的范圍就可能變得非常容易,比如我們用 synchronized 關(guān)鍵字時(shí),如果想把更多的代碼加鎖,那么只需要把更多的代碼放到同步代碼塊里面就可以了。