java.util.concurrent包完全建立在CAS之上的洗贰,沒有CAS就不會有此包≡沙可見CAS的重要性敛滋。
CAS
- CAS:Compare and Swap, 翻譯成比較并交換。
- java.util.concurrent包中借助CAS實現(xiàn)了區(qū)別于synchronouse同步鎖的一種樂觀鎖兴革。
CAS應(yīng)用
CAS有3個操作數(shù)绎晃,內(nèi)存值V,舊的預(yù)期值A(chǔ)杂曲,要修改的新值B庶艾。當(dāng)且僅當(dāng)預(yù)期值A(chǔ)和內(nèi)存值V相同時,將內(nèi)存值V修改為B擎勘,否則什么都不做咱揍。
非阻塞算法 (nonblocking algorithms)
一個線程的失敗或者掛起不應(yīng)該影響其他線程的失敗或掛起的算法
現(xiàn)代的CPU提供了特殊的指令,可以自動更新共享數(shù)據(jù)棚饵,而且能夠檢測到其他線程的干擾煤裙,而 compareAndSet() 就用這些代替了鎖定。
拿出AtomicInteger來研究在沒有鎖的情況下是如何做到數(shù)據(jù)正確性的噪漾。
private volatile int value;
首先毫無以為硼砰,在沒有鎖的機(jī)制下可能需要借助volatile原語,保證線程間的數(shù)據(jù)是可見的(共享的)怪与。這樣才獲取變量的值的時候才能直接讀取夺刑。
public final int get() {
return value;
}
然后來看看++i是怎么做到的。
public final int incrementAndGet() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return next;
}
}
在這里采用了CAS操作,每次從內(nèi)存中讀取數(shù)據(jù)然后將此數(shù)據(jù)和+1后的結(jié)果進(jìn)行CAS操作遍愿,如果成功就返回結(jié)果存淫,否則重試直到成功為止。
而compareAndSet利用JNI來完成CPU指令的操作沼填。
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
整體的過程就是這樣子的桅咆,利用CPU的CAS指令,同時借助JNI來完成Java的非阻塞算法坞笙。其它原子操作都是利用類似的特性完成的岩饼。
其中unsafe.compareAndSwapInt(this, valueOffset, expect, update);
類似:
if (this == expect) {
this = update
return true;
} else {
return false;
}
那么問題就來了,成功過程中需要2個步驟:比較this == expect薛夜,替換this = update籍茧,compareAndSwapInt如何這兩個步驟的原子性呢? 參考CAS的原理梯澜。
CAS原理
CAS通過調(diào)用JNI的代碼實現(xiàn)的寞冯。JNI:Java Native Interface為JAVA本地調(diào)用,允許java調(diào)用其他語言晚伙。
而compareAndSwapInt就是借助C來調(diào)用CPU底層指令實現(xiàn)的吮龄。
下面從分析比較常用的CPU(intel x86)來解釋CAS的實現(xiàn)原理。
下面是sun.misc.Unsafe類的compareAndSwapInt()方法的源代碼:
public final native boolean compareAndSwapInt(Object o, long offset,int expected,int x);
可以看到這是個本地方法調(diào)用咆疗。這個本地方法在openjdk中依次調(diào)用的c++代碼為:unsafe.cpp漓帚,atomic.cpp和atomicwindowsx86.inline.hpp。這個本地方法的最終實現(xiàn)在openjdk的如下位置:openjdk-7-fcs-src-b147-27jun2011\openjdk\hotspot\src\oscpu\windowsx86\vm\ atomicwindowsx86.inline.hpp(對應(yīng)于windows操作系統(tǒng)午磁,X86處理器)尝抖。下面是對應(yīng)于intel x86處理器的源代碼的片段:
// Adding a lock prefix to an instruction on MP machine
// VC++ doesn't like the lock prefix to be on a single line
// so we can't insert a label after the lock prefix.
// By emitting a lock prefix, we can define a label after it.
#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
}
}
如上面源代碼所示,程序會根據(jù)當(dāng)前處理器的類型來決定是否為cmpxchg指令添加lock前綴漓踢。如果程序是在多處理器上運行牵署,就為cmpxchg指令加上lock前綴(lock cmpxchg)。反之喧半,如果程序是在單處理器上運行奴迅,就省略lock前綴(單處理器自身會維護(hù)單處理器內(nèi)的順序一致性,不需要lock前綴提供的內(nèi)存屏障效果)挺据。
intel的手冊對lock前綴的說明如下:
- 確保對內(nèi)存的讀-改-寫操作原子執(zhí)行取具。在Pentium及Pentium之前的處理器中,帶有l(wèi)ock前綴的指令在執(zhí)行期間會鎖住總線扁耐,使得其他處理器暫時無法通過總線訪問內(nèi)存暇检。很顯然,這會帶來昂貴的開銷婉称。從Pentium 4块仆,Intel Xeon及P6處理器開始构蹬,intel在原有總線鎖的基礎(chǔ)上做了一個很有意義的優(yōu)化:如果要訪問的內(nèi)存區(qū)域(area of memory)在lock前綴指令執(zhí)行期間已經(jīng)在處理器內(nèi)部的緩存中被鎖定(即包含該內(nèi)存區(qū)域的緩存行當(dāng)前處于獨占或以修改狀態(tài)),并且該內(nèi)存區(qū)域被完全包含在單個緩存行(cache line)中悔据,那么處理器將直接執(zhí)行該指令庄敛。由于在指令執(zhí)行期間該緩存行會一直被鎖定,其它處理器無法讀/寫該指令要訪問的內(nèi)存區(qū)域科汗,因此能保證指令執(zhí)行的原子性藻烤。這個操作過程叫做緩存鎖定(cache locking),緩存鎖定將大大降低lock前綴指令的執(zhí)行開銷头滔,但是當(dāng)多處理器之間的競爭程度很高或者指令訪問的內(nèi)存地址未對齊時怖亭,仍然會鎖住總線。
- 禁止該指令與之前和之后的讀和寫指令重排序坤检。
- 把寫緩沖區(qū)中的所有數(shù)據(jù)刷新到內(nèi)存中兴猩。
備注知識:
關(guān)于CPU的鎖有如下3種:
- 處理器自動保證基本內(nèi)存操作的原子性
首先處理器會自動保證基本的內(nèi)存操作的原子性。處理器保證從系統(tǒng)內(nèi)存當(dāng)中讀取或者寫入一個字節(jié)是原子的缀蹄,意思是當(dāng)一個處理器讀取一個字節(jié)時峭跳,其他處理器不能訪問這個字節(jié)的內(nèi)存地址膘婶。奔騰6和最新的處理器能自動保證單處理器對同一個緩存行里進(jìn)行16/32/64位的操作是原子的缺前,但是復(fù)雜的內(nèi)存操作處理器不能自動保證其原子性,比如跨總線寬度悬襟,跨多個緩存行衅码,跨頁表的訪問。但是處理器提供總線鎖定和緩存鎖定兩個機(jī)制來保證復(fù)雜內(nèi)存操作的原子性脊岳。
- 使用總線鎖保證原子性
第一個機(jī)制是通過總線鎖保證原子性逝段。如果多個處理器同時對共享變量進(jìn)行讀改寫(i++就是經(jīng)典的讀改寫操作)操作,那么共享變量就會被多個處理器同時進(jìn)行操作割捅,這樣讀改寫操作就不是原子的奶躯,操作完之后共享變量的值會和期望的不一致,舉個例子:如果i=1,我們進(jìn)行兩次i++操作亿驾,我們期望的結(jié)果是3嘹黔,但是有可能結(jié)果是2。如下圖
[圖片上傳失敗...(image-ffe264-1524393847658)]
原因是有可能多個處理器同時從各自的緩存中讀取變量i莫瞬,分別進(jìn)行加一操作儡蔓,然后分別寫入系統(tǒng)內(nèi)存當(dāng)中。那么想要保證讀改寫共享變量的操作是原子的疼邀,就必須保證CPU1讀改寫共享變量的時候喂江,CPU2不能操作緩存了該共享變量內(nèi)存地址的緩存。
處理器使用總線鎖就是來解決這個問題的旁振。所謂總線鎖就是使用處理器提供的一個LOCK#信號获询,當(dāng)一個處理器在總線上輸出此信號時涨岁,其他處理器的請求將被阻塞住,那么該處理器可以獨占使用共享內(nèi)存。
- 使用緩存鎖保證原子性
第二個機(jī)制是通過緩存鎖定保證原子性吉嚣。在同一時刻我們只需保證對某個內(nèi)存地址的操作是原子性即可卵惦,但總線鎖定把CPU和內(nèi)存之間通信鎖住了,這使得鎖定期間瓦戚,其他處理器不能操作其他內(nèi)存地址的數(shù)據(jù)沮尿,所以總線鎖定的開銷比較大,最近的處理器在某些場合下使用緩存鎖定代替總線鎖定來進(jìn)行優(yōu)化较解。
頻繁使用的內(nèi)存會緩存在處理器的L1畜疾,L2和L3高速緩存里,那么原子操作就可以直接在處理器內(nèi)部緩存中進(jìn)行印衔,并不需要聲明總線鎖啡捶,在奔騰6和最近的處理器中可以使用“緩存鎖定”的方式來實現(xiàn)復(fù)雜的原子性。所謂“緩存鎖定”就是如果緩存在處理器緩存行中內(nèi)存區(qū)域在LOCK操作期間被鎖定奸焙,當(dāng)它執(zhí)行鎖操作回寫內(nèi)存時瞎暑,處理器不在總線上聲言LOCK#信號,而是修改內(nèi)部的內(nèi)存地址与帆,并允許它的緩存一致性機(jī)制來保證操作的原子性了赌,因為緩存一致性機(jī)制會阻止同時修改被兩個以上處理器緩存的內(nèi)存區(qū)域數(shù)據(jù),當(dāng)其他處理器回寫已被鎖定的緩存行的數(shù)據(jù)時會起緩存行無效玄糟,在例1中勿她,當(dāng)CPU1修改緩存行中的i時使用緩存鎖定,那么CPU2就不能同時緩存了i的緩存行阵翎。
但是有兩種情況下處理器不會使用緩存鎖定逢并。第一種情況是:當(dāng)操作的數(shù)據(jù)不能被緩存在處理器內(nèi)部,或操作的數(shù)據(jù)跨多個緩存行(cache line)郭卫,則處理器會調(diào)用總線鎖定砍聊。第二種情況是:有些處理器不支持緩存鎖定。對于Inter486和奔騰處理器,就算鎖定的內(nèi)存區(qū)域在處理器的緩存行中也會調(diào)用總線鎖定贰军。
以上兩個機(jī)制我們可以通過Inter處理器提供了很多LOCK前綴的指令來實現(xiàn)玻蝌。比如位測試和修改指令BTS,BTR谓形,BTC灶伊,交換指令XADD,CMPXCHG和其他一些操作數(shù)和邏輯指令寒跳,比如ADD(加)聘萨,OR(或)等,被這些指令操作的內(nèi)存區(qū)域就會加鎖童太,導(dǎo)致其他處理器不能同時訪問它米辐。
CAS缺點
CAS雖然很高效的解決原子操作胸完,但是CAS仍然存在三大問題。ABA問題翘贮,循環(huán)時間長開銷大和只能保證一個共享變量的原子操作
-
ABA問題赊窥。因為CAS需要在操作值的時候檢查下值有沒有發(fā)生變化,如果沒有發(fā)生變化則更新狸页,但是如果一個值原來是A锨能,變成了B,又變成了A芍耘,那么使用CAS進(jìn)行檢查時會發(fā)現(xiàn)它的值沒有發(fā)生變化址遇,但是實際上卻變化了。ABA問題的解決思路就是使用版本號斋竞。在變量前面追加上版本號倔约,每次變量更新的時候把版本號加一,那么A-B-A 就會變成1A-2B-3A坝初。
從Java1.5開始JDK的atomic包里提供了一個類AtomicStampedReference來解決ABA問題浸剩。這個類的compareAndSet方法作用是首先檢查當(dāng)前引用是否等于預(yù)期引用,并且當(dāng)前標(biāo)志是否等于預(yù)期標(biāo)志鳄袍,如果全部相等绢要,則以原子方式將該引用和該標(biāo)志的值設(shè)置為給定的更新值。
關(guān)于ABA問題參考文檔: http://blog.hesey.net/2011/09/resolve-aba-by-atomicstampedreference.html
循環(huán)時間長開銷大畦木。自旋CAS如果長時間不成功袖扛,會給CPU帶來非常大的執(zhí)行開銷。如果JVM能支持處理器提供的pause指令那么效率會有一定的提升十籍,pause指令有兩個作用,第一它可以延遲流水線執(zhí)行指令(de-pipeline),使CPU不會消耗過多的執(zhí)行資源唇礁,延遲的時間取決于具體實現(xiàn)的版本勾栗,在一些處理器上延遲時間是零。第二它可以避免在退出循環(huán)的時候因內(nèi)存順序沖突(memory order violation)而引起CPU流水線被清空(CPU pipeline flush)盏筐,從而提高CPU的執(zhí)行效率围俘。
只能保證一個共享變量的原子操作。當(dāng)對一個共享變量執(zhí)行操作時琢融,我們可以使用循環(huán)CAS的方式來保證原子操作界牡,但是對多個共享變量操作時,循環(huán)CAS就無法保證操作的原子性漾抬,這個時候就可以用鎖宿亡,或者有一個取巧的辦法,就是把多個共享變量合并成一個共享變量來操作纳令。比如有兩個共享變量i=2,j=a挽荠,合并一下ij=2a克胳,然后用CAS來操作ij。從Java1.5開始JDK提供了AtomicReference類來保證引用對象之間的原子性圈匆,你可以把多個變量放在一個對象里來進(jìn)行CAS操作漠另。
concurrent包的實現(xiàn)
由于java的CAS同時具有 volatile 讀和volatile寫的內(nèi)存語義,因此Java線程之間的通信現(xiàn)在有了下面四種方式:
- A線程寫volatile變量跃赚,隨后B線程讀這個volatile變量笆搓。
- A線程寫volatile變量,隨后B線程用CAS更新這個volatile變量纬傲。
- A線程用CAS更新一個volatile變量砚作,隨后B線程用CAS更新這個volatile變量。
- A線程用CAS更新一個volatile變量嘹锁,隨后B線程讀這個volatile變量葫录。
Java的CAS會使用現(xiàn)代處理器上提供的高效機(jī)器級別原子指令,這些原子指令以原子方式對內(nèi)存執(zhí)行讀-改-寫操作领猾,這是在多處理器中實現(xiàn)同步的關(guān)鍵(從本質(zhì)上來說米同,能夠支持原子性讀-改-寫指令的計算機(jī)器,是順序計算圖靈機(jī)的異步等價機(jī)器摔竿,因此任何現(xiàn)代的多處理器都會去支持某種能對內(nèi)存執(zhí)行原子性讀-改-寫操作的原子指令)面粮。同時,volatile變量的讀/寫和CAS可以實現(xiàn)線程之間的通信。把這些特性整合在一起,就形成了整個concurrent包得以實現(xiàn)的基石舟山。如果我們仔細(xì)分析concurrent包的源代碼實現(xiàn)藐不,會發(fā)現(xiàn)一個通用化的實現(xiàn)模式:
- 首先,聲明共享變量為volatile倍试;
- 然后,使用CAS的原子條件更新來實現(xiàn)線程之間的同步;
- 同時柄驻,配合以volatile的讀/寫和CAS所具有的volatile讀和寫的內(nèi)存語義來實現(xiàn)線程之間的通信。
AQS焙压,非阻塞數(shù)據(jù)結(jié)構(gòu)和原子變量類(java.util.concurrent.atomic包中的類)鸿脓,這些concurrent包中的基礎(chǔ)類都是使用這種模式來實現(xiàn)的,而concurrent包中的高層類又是依賴于這些基礎(chǔ)類來實現(xiàn)的涯曲。從整體來看野哭,concurrent包的實現(xiàn)示意圖如下:
[圖片上傳失敗...(image-c0f73-1524393847658)]