CAS的英文為Compare and Swap 翻譯為比較并交換。
CAS加volatile關(guān)鍵字是實(shí)現(xiàn)并發(fā)包的基石。沒(méi)有CAS就不會(huì)有并發(fā)包,synchronized是一種獨(dú)占鎖、悲觀鎖,java.util.concurrent中借助了CAS指令實(shí)現(xiàn)了一種區(qū)別于synchronized的一種樂(lè)觀鎖。
樂(lè)觀鎖和悲觀鎖是一種概念和思想:
悲觀鎖:總是假設(shè)最壞的情況,每次去拿數(shù)據(jù)的時(shí)候都認(rèn)為別人會(huì)修改,所以每次在拿數(shù)據(jù)的時(shí)候都會(huì)上鎖,這樣當(dāng)?shù)诙€(gè)線程想拿這個(gè)數(shù)據(jù)的時(shí)候,第二個(gè)線程會(huì)一直堵塞,直到第一個(gè)釋放鎖,他拿到鎖后才可以訪問(wèn)。傳統(tǒng)的數(shù)據(jù)庫(kù)里面就用到了這種鎖機(jī)制,例如:行鎖,表鎖,讀鎖,寫(xiě)鎖,都是在操作前先上鎖。java中的synchronized的實(shí)現(xiàn)也是一種悲觀鎖。
樂(lè)觀鎖:樂(lè)觀鎖概念為,每次拿數(shù)據(jù)的時(shí)候都認(rèn)為別的線程不會(huì)修改這個(gè)數(shù)據(jù),所以不會(huì)上鎖,但是在更新的時(shí)候會(huì)判斷一下在此期間別的線程有沒(méi)有修改過(guò)數(shù)據(jù),樂(lè)觀鎖適用于讀操作多的場(chǎng)景,這樣可以提高程序的吞吐量。在Java中
java.util.concurrent.atomic包下面的原子變量就是使用了樂(lè)觀鎖的一種實(shí)現(xiàn)方式CAS實(shí)現(xiàn)。
在JDK 5之前Java語(yǔ)言是靠 synchronized 關(guān)鍵字保證同步的,這會(huì)導(dǎo)致有鎖。鎖機(jī)制存在以下問(wèn)題:
Volatile關(guān)鍵字能夠在并發(fā)條件下,強(qiáng)制將修改后的值刷新到主內(nèi)存中來(lái)保持內(nèi)存的可見(jiàn)性。通過(guò) CPU內(nèi)存屏障禁止編譯器指令性重排來(lái)保證并發(fā)操作的有序性
如果多個(gè)線程同時(shí)操作 Volatile 修飾的變量,也會(huì)造成數(shù)據(jù)的不一致。
public class Test { public volatile int inc = 0; public void increase() { inc++; } public static void main(String[] args) { final Test test = new Test(); for(int i=0;i<10;i++){ new Thread(){ public void run() { for(int j=0;j<1000;j++) test.increase(); }; }.start(); } while(Thread.activeCount()>1) Thread.yield(); System.out.println(test.inc); }}
事實(shí)上運(yùn)行它會(huì)發(fā)現(xiàn)每次運(yùn)行結(jié)果都不一致,都是一個(gè)小于10000的數(shù)字。
假如某個(gè)時(shí)刻變量 inc 的值為10:
之所以出現(xiàn)還是 volatile 只是保證讀寫(xiě)具有原子性,但是對(duì)于 ++ 操作的復(fù)合操作是不存在原子操作的。只能在有限的一些情形下使用 volatile 變量替代鎖。要使 volatile 變量提供理想的線程安全,比如:對(duì)變量的寫(xiě)操作不依賴于當(dāng)前值。
volatile 是不錯(cuò)的機(jī)制,但是 volatile 不能保證原子性。因此對(duì)于同步最終還是要回到鎖機(jī)制上來(lái)。
獨(dú)占鎖是一種悲觀鎖,synchronized 就是一種獨(dú)占鎖,會(huì)導(dǎo)致其它所有需要鎖的線程掛起,等待持有鎖的線程釋放鎖。而另一個(gè)更加有效的鎖就是樂(lè)觀鎖。所謂樂(lè)觀鎖就是,每次不加鎖而是假設(shè)沒(méi)有沖突而去完成某項(xiàng)操作,如果因?yàn)闆_突失敗就重試,直到成功為止。樂(lè)觀鎖用到的機(jī)制就是 CAS。
CAS 操作包含三個(gè)操作數(shù) -- 內(nèi)存位置、預(yù)期數(shù)值和新值。CAS 的實(shí)現(xiàn)邏輯是將內(nèi)存位置處的數(shù)值與預(yù)期數(shù)值想比較,若相等,則將內(nèi)存位置處的值替換為新值。若不相等,則不做任何操作。
在 Java 中,Java 并沒(méi)有直接實(shí)現(xiàn) CAS,CAS 相關(guān)的實(shí)現(xiàn)是通過(guò) C++ 內(nèi)聯(lián)匯編的形式實(shí)現(xiàn)的。Java 代碼需通過(guò) JNI 才能調(diào)用。
在JVM中的CAS操作就是基于處理器的CMPXCHG匯編指令實(shí)現(xiàn)的,因此,JVM中的CAS的原子性是處理器保障的
CAS 是一條 CPU 的原子指令(cmpxchg指令),不會(huì)造成所謂的數(shù)據(jù)不一致問(wèn)題,Unsafe 提供的 CAS 方法(如compareAndSwapXXX)底層實(shí)現(xiàn)即為 CPU 指令 cmpxchg
對(duì)
java.util.concurrent.atomic 包下的原子類 AtomicInteger 中的 compareAndSet 方法進(jìn)行分析,相關(guān)分析如下:
public class AtomicInteger extends Number implements java.io.Serializable { // setup to use Unsafe.compareAndSwapInt for updates private static final Unsafe unsafe = Unsafe.getUnsafe(); private static final long valueOffset; static { try { // 計(jì)算變量 value 在類對(duì)象中的偏移 valueOffset = unsafe.objectFieldOffset (AtomicInteger.class.getDeclaredField("value")); } catch (Exception ex) { throw new Error(ex); } } private volatile int value; public final boolean compareAndSet(int expect, int update) { /** * compareAndSet 實(shí)際上只是一個(gè)殼子,主要的邏輯封裝在 Unsafe 的 * compareAndSwapInt 方法中 */ return unsafe.compareAndSwapInt(this, valueOffset, expect, update); } // ......} public final class Unsafe { // compareAndSwapInt 是 native 類型的方法,繼續(xù)往下看 public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x); // ...... }
public class AtomicInteger extends Number implements java.io.Serializable { // setup to use Unsafe.compareAndSwapInt for updates private static final Unsafe unsafe = Unsafe.getUnsafe(); private static final long valueOffset; static { try { // 計(jì)算變量 value 在類對(duì)象中的偏移 valueOffset = unsafe.objectFieldOffset (AtomicInteger.class.getDeclaredField("value")); } catch (Exception ex) { throw new Error(ex); } } private volatile int value; public final boolean compareAndSet(int expect, int update) { /** * compareAndSet 實(shí)際上只是一個(gè)殼子,主要的邏輯封裝在 Unsafe 的 * compareAndSwapInt 方法中 */ return unsafe.compareAndSwapInt(this, valueOffset, expect, update); } // ......} public final class Unsafe { // compareAndSwapInt 是 native 類型的方法,繼續(xù)往下看 public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x); // ...... }
上面的分析看起來(lái)比較多,不過(guò)主流程并不復(fù)雜。如果不糾結(jié)于代碼細(xì)節(jié),還是比較容易看懂的。接下來(lái),我會(huì)分析 Windows 平臺(tái)下的 Atomic::cmpxchg 函數(shù)。繼續(xù)往下看吧。
// atomic_windows_x86.inline.hpp#define LOCK_IF_MP(mp) __asm cmp mp, 0 / __asm je L0 / __asm _emit 0xF0 / __asm L0: inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) { // alternative for InterlockedCompareExchange int mp = os::is_MP(); __asm { mov edx, dest mov ecx, exchange_value mov eax, compare_value LOCK_IF_MP(mp) cmpxchg dword ptr [edx], ecx }}
上面的代碼由 LOCK_IF_MP 預(yù)編譯標(biāo)識(shí)符和 cmpxchg 函數(shù)組成。為了看到更清楚一些,我們將 cmpxchg 函數(shù)中的 LOCK_IF_MP 替換為實(shí)際內(nèi)容。如下:
inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) { // 判斷是否是多核 CPU int mp = os::is_MP(); __asm { // 將參數(shù)值放入寄存器中 mov edx, dest // 注意: dest 是指針類型,這里是把內(nèi)存地址存入 edx 寄存器中 mov ecx, exchange_value mov eax, compare_value // LOCK_IF_MP cmp mp, 0 /* * 如果 mp = 0,表明是線程運(yùn)行在單核 CPU 環(huán)境下。此時(shí) je 會(huì)跳轉(zhuǎn)到 L0 標(biāo)記處, * 也就是越過(guò) _emit 0xF0 指令,直接執(zhí)行 cmpxchg 指令。也就是不在下面的 cmpxchg 指令 * 前加 lock 前綴。 */ je L0 /* * 0xF0 是 lock 前綴的機(jī)器碼,這里沒(méi)有使用 lock,而是直接使用了機(jī)器碼的形式。至于這樣做的 * 原因可以參考知乎的一個(gè)回答: * https://www.zhihu.com/question/50878124/answer/123099923 */ _emit 0xF0L0: /* * 比較并交換。簡(jiǎn)單解釋一下下面這條指令,熟悉匯編的朋友可以略過(guò)下面的解釋: * cmpxchg: 即“比較并交換”指令 * dword: 全稱是 double word,在 x86/x64 體系中,一個(gè) * word = 2 byte,dword = 4 byte = 32 bit * ptr: 全稱是 pointer,與前面的 dword 連起來(lái)使用,表明訪問(wèn)的內(nèi)存單元是一個(gè)雙字單元 * [edx]: [...] 表示一個(gè)內(nèi)存單元,edx 是寄存器,dest 指針值存放在 edx 中。 * 那么 [edx] 表示內(nèi)存地址為 dest 的內(nèi)存單元 * * 這一條指令的意思就是,將 eax 寄存器中的值(compare_value)與 [edx] 雙字內(nèi)存單元中的值 * 進(jìn)行對(duì)比,如果相同,則將 ecx 寄存器中的值(exchange_value)存入 [edx] 內(nèi)存單元中。 */ cmpxchg dword ptr [edx], ecx }}
到這里 CAS 的實(shí)現(xiàn)過(guò)程就講完了,CAS 的實(shí)現(xiàn)離不開(kāi)處理器的支持。如上面源代碼所示,程序會(huì)根據(jù)當(dāng)前處理器的類型來(lái)決定是否為 cmpxchg 指令添加 lock 前綴。如果程序是在多處理器上運(yùn)行,就為 cmpxchg 指令加上 lock 前綴(lock cmpxchg)。反之,如果程序是在單處理器上運(yùn)行,就省略 lock 前綴(單處理器自身會(huì)維護(hù)單處理器內(nèi)的順序一致性,不需要 lock 前綴提供的內(nèi)存屏障效果)。
intel 的手冊(cè)對(duì) lock 前綴的說(shuō)明如下:
上面的第 2 點(diǎn)和第 3 點(diǎn)所具有的內(nèi)存屏障效果,足以同時(shí)實(shí)現(xiàn) volatile 讀和 volatile 寫(xiě)的內(nèi)存語(yǔ)義。
經(jīng)過(guò)上面的這些分析,現(xiàn)在我們終于能明白為什么 JDK 文檔說(shuō) CAS 同時(shí)具有 volatile 讀和 volatile 寫(xiě)的內(nèi)存語(yǔ)義了。
Java 的 CAS 會(huì)使用現(xiàn)代處理器上提供的高效機(jī)器級(jí)別原子指令,這些原子指令以原子方式對(duì)內(nèi)存執(zhí)行讀 - 改 - 寫(xiě)操作,這是在多處理器中實(shí)現(xiàn)同步的關(guān)鍵(從本質(zhì)上來(lái)說(shuō),能夠支持原子性讀 - 改 - 寫(xiě)指令的計(jì)算機(jī)器,是順序計(jì)算圖靈機(jī)的異步等價(jià)機(jī)器,因此任何現(xiàn)代的多處理器都會(huì)去支持某種能對(duì)內(nèi)存執(zhí)行原子性讀 - 改 - 寫(xiě)操作的原子指令)。同時(shí),volatile 變量的讀 / 寫(xiě)和 CAS 可以實(shí)現(xiàn)線程之間的通信。把這些特性整合在一起,就形成了整個(gè) concurrent 包得以實(shí)現(xiàn)的基石。如果我們仔細(xì)分析 concurrent 包的源代碼實(shí)現(xiàn),會(huì)發(fā)現(xiàn)一個(gè)通用化的實(shí)現(xiàn)模式:
AQS,非阻塞數(shù)據(jù)結(jié)構(gòu)和原子變量類(
java.util.concurrent.atomic 包中的類),這些 concurrent 包中的基礎(chǔ)類都是使用這種模式來(lái)實(shí)現(xiàn)的,而 concurrent 包中的高層類又是依賴于這些基礎(chǔ)類來(lái)實(shí)現(xiàn)的。從整體來(lái)看,concurrent 包的實(shí)現(xiàn)示意圖如下:
JVM中的CAS(堆中對(duì)象的分配):
Java 調(diào)用 new object() 會(huì)創(chuàng)建一個(gè)對(duì)象,這個(gè)對(duì)象會(huì)被分配到 JVM 的堆中。那么這個(gè)對(duì)象到底是怎么在堆中保存的呢?
首先,new object() 執(zhí)行的時(shí)候,這個(gè)對(duì)象需要多大的空間,其實(shí)是已經(jīng)確定的,因?yàn)?java 中的各種數(shù)據(jù)類型,占用多大的空間都是固定的(對(duì)其原理不清楚的請(qǐng)自行Google)。那么接下來(lái)的工作就是在堆中找出那么一塊空間用于存放這個(gè)對(duì)象。
在單線程的情況下,一般有兩種分配策略:
但是JVM不可能一直在單線程狀態(tài)下運(yùn)行,那樣效率太差了。由于再給一個(gè)對(duì)象分配內(nèi)存的時(shí)候不是原子性的操作,至少需要以下幾步:查找空閑列表、分配內(nèi)存、修改空閑列表等等,這是不安全的。解決并發(fā)時(shí)的安全問(wèn)題也有兩種策略:
虛擬機(jī)是否使用TLAB,可以通過(guò)-XX:+/-UseTLAB參數(shù)來(lái)進(jìn)行配置(jdk5及以后的版本默認(rèn)是啟用TLAB的)。
談到 CAS,基本上都要談一下 CAS 的 ABA 問(wèn)題。CAS 由三個(gè)步驟組成,分別是“讀取-比較-寫(xiě)回”。考慮這樣一種情況,線程1和線程2同時(shí)執(zhí)行 CAS 邏輯,兩個(gè)線程的執(zhí)行順序如下:
然后用新值(newValue)寫(xiě)入內(nèi)存中,完成 CAS 操作
如上流程,線程1并不知道原值已經(jīng)被修改過(guò)了,在它看來(lái)并沒(méi)什么變化,所以它會(huì)繼續(xù)往下執(zhí)行流程。對(duì)于 ABA 問(wèn)題,通常的處理措施是對(duì)每一次 CAS 操作設(shè)置版本號(hào)。
ABA問(wèn)題的解決思路其實(shí)也很簡(jiǎn)單,就是使用版本號(hào)。在變量前面追加上版本號(hào),每次變量更新的時(shí)候把版本號(hào)加1,那么A→B→A就會(huì)變成1A→2B→3A了。
java.util.concurrent.atomic 包下提供了一個(gè)可處理 ABA 問(wèn)題的原子類 AtomicStampedReference,
從Java1.5開(kāi)始JDK的atomic包里提供了一個(gè)類AtomicStampedReference來(lái)解決ABA問(wèn)題。這個(gè)類的compareAndSet方法作用是首先檢查當(dāng)前引用是否等于預(yù)期引用,并且當(dāng)前標(biāo)志是否等于預(yù)期標(biāo)志,如果全部相等,則以原子方式將該引用和該標(biāo)志的值設(shè)置為給定的更新值。
自旋CAS(不成功,就一直循環(huán)執(zhí)行,直到成功) 如果長(zhǎng)時(shí)間不成功,會(huì)給 CPU 帶來(lái)非常大的執(zhí)行開(kāi)銷。如果JVM能支持處理器提供的 pause 指令那么效率會(huì)有一定的提升,pause指令有兩個(gè)作用,第一它可以延遲流水線執(zhí)行指令(de-pipeline),使 CPU 不會(huì)消耗過(guò)多的執(zhí)行資源,延遲的時(shí)間取決于具體實(shí)現(xiàn)的版本,在一些處理器上延遲時(shí)間是零。第二它可以避免在退出循環(huán)的時(shí)候因內(nèi)存順序沖突(memory order violation)而引起 CPU 流水線被清空(CPU pipeline flush),從而提高 CPU 的執(zhí)行效率。
當(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 操作。
CAS 與 Synchronized 的使用情景:
補(bǔ)充: synchronized 在 jdk1.6 之后,已經(jīng)改進(jìn)優(yōu)化。synchronized 的底層實(shí)現(xiàn)主要依靠 Lock-Free 的隊(duì)列,基本思路是自旋后阻塞,競(jìng)爭(zhēng)切換后繼續(xù)競(jìng)爭(zhēng)鎖,稍微犧牲了公平性,但獲得了高吞吐量。在線程沖突較少的情況下,可以獲得和 CAS 類似的性能;而線程沖突嚴(yán)重的情況下,性能遠(yuǎn)高于 CAS。
本文鏈接:http://www.tebozhan.com/showinfo-26-13497-0.html并發(fā)樂(lè)觀鎖CAS原理,吊打問(wèn)并發(fā)的面試官
聲明:本網(wǎng)頁(yè)內(nèi)容旨在傳播知識(shí),若有侵權(quán)等問(wèn)題請(qǐng)及時(shí)與本網(wǎng)聯(lián)系,我們將在第一時(shí)間刪除處理。郵件:2376512515@qq.com