1.原子操作意為“不可被中斷的一個(gè)或一系列操作”樟插。再多處理器上實(shí)現(xiàn)原子操作就變的有點(diǎn)復(fù)雜绅作。
2.處理器如何實(shí)現(xiàn)原子操作郊楣?
? ? ? ?首先處理器能自動(dòng)保證基本的內(nèi)存操作是原子性的。表示當(dāng)一個(gè)處理器讀取一個(gè)字節(jié)時(shí)烁挟,其他處理器不能訪問(wèn)這個(gè)字節(jié)的內(nèi)存地址。但是對(duì)于復(fù)雜的內(nèi)存操作處理器是不能自動(dòng)保證其原子性的骨坑,比如跨總線寬度撼嗓、跨多個(gè)緩存行和跨頁(yè)表的訪問(wèn)。但是欢唾,處理器提供了總線鎖定和緩存鎖定兩個(gè)機(jī)制來(lái)保證復(fù)雜內(nèi)存操作的原子性且警。
3.總線鎖定和緩存鎖定
? ? ?-1)使用總線鎖保證原子性
? ? ? 第一機(jī)制是通過(guò)總線鎖保證原子性。如果多個(gè)處理器同時(shí)對(duì)共享變量進(jìn)行讀改寫(xiě)操作(i++就是經(jīng)典的讀改寫(xiě)操作)礁遣,那么共享變量就會(huì)被多個(gè)處理器同時(shí)進(jìn)行操作斑芜,這樣讀改寫(xiě)操作就不是原子的,操作完之后的共享變量的值會(huì)和期望的不一致祟霍。舉個(gè)例子杏头,如果i=1,我們進(jìn)行兩次i++操作沸呐,我們期望的結(jié)果是3醇王,但是有可能結(jié)果是2。? ?原因可能是多個(gè)處理器同時(shí)從各自的緩存中讀取變量i垂谢,分別進(jìn)行加1操作厦画,然后分別寫(xiě)入系統(tǒng)內(nèi)存中。那么滥朱,想要保證讀改寫(xiě)共享變量的操作是原子的根暑,就必須保證CPU1讀改寫(xiě)共享變量的時(shí)候,CPU2不能操作緩存了該共享變量?jī)?nèi)存地址的緩存徙邻。
? ? ? ?處理器使用總線鎖就是來(lái)解決這個(gè)問(wèn)題的排嫌。所謂總線鎖就是使用處理器提供的一個(gè)LOCLK#信號(hào),當(dāng)一個(gè)處理器在總線上輸出此信號(hào)時(shí)缰犁,其他處理器的請(qǐng)求將被阻塞住淳地,那么該處理器可以獨(dú)占共享內(nèi)存。
? ? ?-2)使用緩存鎖保證原子性
? ? ? 第二機(jī)制是通過(guò)緩存鎖定來(lái)保證原子性帅容。在同一時(shí)刻颇象,只需保證對(duì)某個(gè)內(nèi)存地址的操作是原子性即可,但總線鎖定把CPU和內(nèi)存之間的通信鎖住了并徘,這使得鎖定期間遣钳,其它處理器不能操作其他內(nèi)存地址的數(shù)據(jù),所以總線鎖定的開(kāi)銷(xiāo)比較大麦乞,目前處理器在某些場(chǎng)合下使用緩存鎖定代替總線鎖定來(lái)進(jìn)行優(yōu)化蕴茴。頻繁使用的內(nèi)存會(huì)緩存在處理器的L1劝评、L2和L3高速緩存里,那么原子操作就可以直接在處理器內(nèi)部緩存中進(jìn)行倦淀,并不需要聲明總線鎖蒋畜,在Pentium6和目前的處理器中可以使用“緩存鎖定”的方式來(lái)實(shí)現(xiàn)復(fù)雜的原子性。
? ? ?所謂“緩存鎖定”是指如果共享內(nèi)存如果被換存在處理器的緩存行中撞叽,并且在Lock操作期間被鎖定姻成,那么當(dāng)它執(zhí)行鎖操作回寫(xiě)到內(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)其他處理器回寫(xiě)已被鎖定的緩存行的數(shù)據(jù)時(shí)辛润,會(huì)使緩存行無(wú)效。
? ? ?但是有兩種情況下處理器不會(huì)使用緩存鎖定
? ? ?第一種情況是:當(dāng)操作的數(shù)據(jù)不能被緩存在處理器內(nèi)部见秤,或操作的數(shù)據(jù)跨多個(gè)緩存行時(shí)砂竖,則處理器會(huì)調(diào)用總線鎖定。
? ? ?第二種情況是:有些處理器不支持緩存鎖定鹃答。對(duì)于Intel486和Pentium處理器乎澄,就算鎖定的內(nèi)存區(qū)域在處理器的緩存行中也會(huì)調(diào)用總線鎖定。
? ? ?針對(duì)以上兩個(gè)機(jī)制测摔,我們通過(guò)Intel處理器提供了很多Lock前綴的指令來(lái)實(shí)現(xiàn)置济。例如,位測(cè)試和修改指令:BTS锋八、BTR浙于、BTC;交換指令XADD挟纱、CMPXCHG羞酗,以及其他一些操作數(shù)和邏輯指令。被這些指令操作的內(nèi)存區(qū)域就會(huì)加鎖紊服,導(dǎo)致其他處理器不能同時(shí)訪問(wèn)它檀轨。
4.CAS
在Java中可以通過(guò)鎖和循環(huán)CAS的方式來(lái)實(shí)現(xiàn)原子操作。
? (1)使用循環(huán)CAS實(shí)現(xiàn)原子操作
? ? 使用CAS操作可以不加鎖的實(shí)現(xiàn)原子操作欺嗤,如i++操作在多線程下参萄,i處于一種不穩(wěn)定的狀態(tài)。使用鎖可以解決同步問(wèn)題煎饼,但是也可以使用CAS操作也可以實(shí)現(xiàn)操作的原子性拧揽。代碼如下
intexpect = a;
if(a.compareAndSet(expect,a+1)) {?
?????????doSomeThing1();
}else{
?doSomeThing2();
}
? ?這樣如果a的值被改變了a++就不會(huì)被執(zhí)行。按照上面的寫(xiě)法,a!=expect之后,a++就不會(huì)被執(zhí)行淤袜,如果我們還是想執(zhí)行a++操作怎么辦,沒(méi)關(guān)系衰伯,可以采用while循環(huán)
while(true) {
????intexpect = a;
????if(a.compareAndSet(expect, a +1)) {
?????????doSomeThing1();
????????return;?
?????}else{?
?????????doSomeThing2();?
?????}
}
? ?采用上面的寫(xiě)法铡羡,在沒(méi)有鎖的情況下實(shí)現(xiàn)了a++操作,這實(shí)際上是一種非阻塞算法意鲸。
? ?CAS有3個(gè)操作數(shù)烦周,內(nèi)存值V,舊的預(yù)期值A(chǔ)怎顾,要修改的新值B读慎。當(dāng)且僅當(dāng)預(yù)期值A(chǔ)和內(nèi)存值V相同時(shí),將內(nèi)存值V修改為B槐雾,否則什么都不做夭委。
? ?成功過(guò)程中需要2個(gè)步驟:比較this == expect,替換this = update募强,compareAndSwapInt如何這兩個(gè)步驟的原子性呢株灸?
? ?JVM中的CAS操作是利用了處理器提供的CMPXCHG指令實(shí)現(xiàn)的。自旋CAS實(shí)現(xiàn)的基本思路就是循環(huán)進(jìn)行CAS操作直到成功為止擎值。
? ?CAS通過(guò)調(diào)用JNI的代碼實(shí)現(xiàn)的慌烧。JNI:Java Native Interface為JAVA本地調(diào)用,允許java調(diào)用其他語(yǔ)言鸠儿。而compareAndSwapInt就是借助C來(lái)調(diào)用CPU底層指令實(shí)現(xiàn)的屹蚊。程序會(huì)根據(jù)當(dāng)前處理器的類(lèi)型來(lái)決定是否為cmpxchg指令添加lock前綴。如果程序是在多處理器上運(yùn)行进每,就為cmpxchg指令加上lock前綴(lock cmpxchg)汹粤。反之,如果程序是在單處理器上運(yùn)行品追,就省略lock前綴(單處理器自身會(huì)維護(hù)單處理器內(nèi)的順序一致性玄括,不需要lock前綴提供的內(nèi)存屏障效果)。
? ? 5.CAS實(shí)現(xiàn)原子操作的三大問(wèn)題
在Java并發(fā)包中有一些并發(fā)框架也使用了自旋CAS的方式來(lái)實(shí)現(xiàn)原子操作肉瓦,比如LinkedTransferQueue類(lèi)的Xfer方法遭京。CAS雖然很高效地解決了原子操作,但是CAS仍然存在三大問(wèn)題泞莉。ABA問(wèn)題哪雕,循環(huán)時(shí)間長(zhǎng)開(kāi)銷(xiāo)大,以及只能保證一個(gè)共享變量的原子操作鲫趁。
? ? ? ?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)加1,那么A->B->A就會(huì)變成1A->2B->3A城菊。從Java1.5開(kāi)始备燃,JDK的Atomic包里提供了一個(gè)類(lèi)AtomicStampedReference來(lái)解決ABA問(wèn)題。這個(gè)類(lèi)的compareAndSet方法的作用是首先檢查當(dāng)前引用是否等于預(yù)期引用凌唬,并且檢查當(dāng)前標(biāo)志是否等于預(yù)期標(biāo)志并齐,如果全部相等,則以原子方式將該飲用和該標(biāo)志的值設(shè)置為給定的更新值法瑟。
public boolean compareAndSet{
? ? ? ? V? ? expectedReference冀膝,? ? ? ? //預(yù)期引用
? ? ? ? V? ? newReference,? ? ? ? ? ? ? ? //更新后的引用
? ? ? ?int? ? expectedStamp? ? ? ? ? ? ? ? ?//預(yù)期標(biāo)志
? ? ? ?int? ? newStamp? ? ? ? ? ? ? ? ? ? ? ? ?//更新后的標(biāo)志
}
? ? ?2)循環(huán)時(shí)間長(zhǎng)開(kāi)銷(xiāo)大霎挟。自旋CAS如果長(zhǎng)時(shí)間不成功窝剖,會(huì)給CPU帶來(lái)非常大的執(zhí)行開(kāi)銷(xiāo)。如果JVM能支持處理器提供的pause指令酥夭,那么效率會(huì)有一定的提升赐纱。pause指令有兩個(gè)作用:第一,它可以延遲流水線執(zhí)行指令熬北,使CPU不會(huì)消耗過(guò)多的執(zhí)行資源疙描,延遲的時(shí)間取決于具體實(shí)現(xiàn)的版本,在一些處理器上延遲時(shí)間是零讶隐;第二起胰,它可以避免在退出循環(huán)的時(shí)候因內(nèi)存順序沖突而引起CPU流水線被清空,從而提高CPU的執(zhí)行效率巫延。
? ?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=2a疯攒,然后用CAS來(lái)操作ij嗦随。從JDK1.5開(kāi)始提供了AtomicReference類(lèi)來(lái)保證引用對(duì)象之間的原子性,就可以把多個(gè)變量放在一個(gè)對(duì)象里來(lái)進(jìn)行CAS操作敬尺。
? ? ??6.使用鎖機(jī)制實(shí)現(xiàn)原子操作
? ? ?鎖機(jī)制保證了只有獲得鎖的線程才能夠操作鎖定的內(nèi)存區(qū)域。JVM內(nèi)部實(shí)現(xiàn)了很多種鎖機(jī)制贴浙,有偏向鎖砂吞、輕量級(jí)鎖和互斥鎖。有意思的是除了偏向鎖崎溃,JVM實(shí)現(xiàn)鎖的方式都用了循環(huán)CAS蜻直,即當(dāng)一個(gè)線程想進(jìn)入同步塊的時(shí)候使用循環(huán)CAS的方式來(lái)獲取鎖,當(dāng)他退出同步塊的時(shí)候使用循環(huán)CAS釋放鎖袁串。