1.原子操作意為“不可被中斷的一個或一系列操作”。再多處理器上實現原子操作就變的有點復雜。
2.處理器如何實現原子操作?
? ? ? ?首先處理器能自動保證基本的內存操作是原子性的。表示當一個處理器讀取一個字節時,其他處理器不能訪問這個字節的內存地址。但是對于復雜的內存操作處理器是不能自動保證其原子性的,比如跨總線寬度、跨多個緩存行和跨頁表的訪問。但是,處理器提供了總線鎖定和緩存鎖定兩個機制來保證復雜內存操作的原子性。
3.總線鎖定和緩存鎖定
? ? ?-1)使用總線鎖保證原子性
? ? ? 第一機制是通過總線鎖保證原子性。如果多個處理器同時對共享變量進行讀改寫操作(i++就是經典的讀改寫操作),那么共享變量就會被多個處理器同時進行操作,這樣讀改寫操作就不是原子的,操作完之后的共享變量的值會和期望的不一致。舉個例子,如果i=1,我們進行兩次i++操作,我們期望的結果是3,但是有可能結果是2。? ?原因可能是多個處理器同時從各自的緩存中讀取變量i,分別進行加1操作,然后分別寫入系統內存中。那么,想要保證讀改寫共享變量的操作是原子的,就必須保證CPU1讀改寫共享變量的時候,CPU2不能操作緩存了該共享變量內存地址的緩存。
? ? ? ?處理器使用總線鎖就是來解決這個問題的。所謂總線鎖就是使用處理器提供的一個LOCLK#信號,當一個處理器在總線上輸出此信號時,其他處理器的請求將被阻塞住,那么該處理器可以獨占共享內存。
? ? ?-2)使用緩存鎖保證原子性
? ? ? 第二機制是通過緩存鎖定來保證原子性。在同一時刻,只需保證對某個內存地址的操作是原子性即可,但總線鎖定把CPU和內存之間的通信鎖住了,這使得鎖定期間,其它處理器不能操作其他內存地址的數據,所以總線鎖定的開銷比較大,目前處理器在某些場合下使用緩存鎖定代替總線鎖定來進行優化。頻繁使用的內存會緩存在處理器的L1、L2和L3高速緩存里,那么原子操作就可以直接在處理器內部緩存中進行,并不需要聲明總線鎖,在Pentium6和目前的處理器中可以使用“緩存鎖定”的方式來實現復雜的原子性。
? ? ?所謂“緩存鎖定”是指如果共享內存如果被換存在處理器的緩存行中,并且在Lock操作期間被鎖定,那么當它執行鎖操作回寫到內存時,處理器不在總線上聲言LOCK#信號,而是修改內部的內存地址,并允許它的緩存一致性機制來保證操作的原子性,因為緩存一致性機制會阻止同時修改由兩個以上處理器緩存的內存區域數據,當其他處理器回寫已被鎖定的緩存行的數據時,會使緩存行無效。
? ? ?但是有兩種情況下處理器不會使用緩存鎖定
? ? ?第一種情況是:當操作的數據不能被緩存在處理器內部,或操作的數據跨多個緩存行時,則處理器會調用總線鎖定。
? ? ?第二種情況是:有些處理器不支持緩存鎖定。對于Intel486和Pentium處理器,就算鎖定的內存區域在處理器的緩存行中也會調用總線鎖定。
? ? ?針對以上兩個機制,我們通過Intel處理器提供了很多Lock前綴的指令來實現。例如,位測試和修改指令:BTS、BTR、BTC;交換指令XADD、CMPXCHG,以及其他一些操作數和邏輯指令。被這些指令操作的內存區域就會加鎖,導致其他處理器不能同時訪問它。
4.CAS
在Java中可以通過鎖和循環CAS的方式來實現原子操作。
? (1)使用循環CAS實現原子操作
? ? 使用CAS操作可以不加鎖的實現原子操作,如i++操作在多線程下,i處于一種不穩定的狀態。使用鎖可以解決同步問題,但是也可以使用CAS操作也可以實現操作的原子性。代碼如下
intexpect = a;
if(a.compareAndSet(expect,a+1)) {?
?????????doSomeThing1();
}else{
?doSomeThing2();
}
? ?這樣如果a的值被改變了a++就不會被執行。按照上面的寫法,a!=expect之后,a++就不會被執行,如果我們還是想執行a++操作怎么辦,沒關系,可以采用while循環
while(true) {
????intexpect = a;
????if(a.compareAndSet(expect, a +1)) {
?????????doSomeThing1();
????????return;?
?????}else{?
?????????doSomeThing2();?
?????}
}
? ?采用上面的寫法,在沒有鎖的情況下實現了a++操作,這實際上是一種非阻塞算法。
? ?CAS有3個操作數,內存值V,舊的預期值A,要修改的新值B。當且僅當預期值A和內存值V相同時,將內存值V修改為B,否則什么都不做。
? ?成功過程中需要2個步驟:比較this == expect,替換this = update,compareAndSwapInt如何這兩個步驟的原子性呢?
? ?JVM中的CAS操作是利用了處理器提供的CMPXCHG指令實現的。自旋CAS實現的基本思路就是循環進行CAS操作直到成功為止。
? ?CAS通過調用JNI的代碼實現的。JNI:Java Native Interface為JAVA本地調用,允許java調用其他語言。而compareAndSwapInt就是借助C來調用CPU底層指令實現的。程序會根據當前處理器的類型來決定是否為cmpxchg指令添加lock前綴。如果程序是在多處理器上運行,就為cmpxchg指令加上lock前綴(lock cmpxchg)。反之,如果程序是在單處理器上運行,就省略lock前綴(單處理器自身會維護單處理器內的順序一致性,不需要lock前綴提供的內存屏障效果)。
? ? 5.CAS實現原子操作的三大問題
在Java并發包中有一些并發框架也使用了自旋CAS的方式來實現原子操作,比如LinkedTransferQueue類的Xfer方法。CAS雖然很高效地解決了原子操作,但是CAS仍然存在三大問題。ABA問題,循環時間長開銷大,以及只能保證一個共享變量的原子操作。
? ? ? ?1)ABA問題。? 因為CAS需要在操作值的時候,檢查值有沒有發生變化,如果沒有發生變化則更新,但是如果一個值原來是A,變成了B,又變成了A,那么使用CAS進行檢查時會發現它的值沒有發生變化,但是實際上卻變化了。ABA問題的解決思路就是使用版本號。在變量前面追加上版本號,每次變量更新的時候把版本號加1,那么A->B->A就會變成1A->2B->3A。從Java1.5開始,JDK的Atomic包里提供了一個類AtomicStampedReference來解決ABA問題。這個類的compareAndSet方法的作用是首先檢查當前引用是否等于預期引用,并且檢查當前標志是否等于預期標志,如果全部相等,則以原子方式將該飲用和該標志的值設置為給定的更新值。
public boolean compareAndSet{
? ? ? ? V? ? expectedReference,? ? ? ? //預期引用
? ? ? ? V? ? newReference,? ? ? ? ? ? ? ? //更新后的引用
? ? ? ?int? ? expectedStamp? ? ? ? ? ? ? ? ?//預期標志
? ? ? ?int? ? newStamp? ? ? ? ? ? ? ? ? ? ? ? ?//更新后的標志
}
? ? ?2)循環時間長開銷大。自旋CAS如果長時間不成功,會給CPU帶來非常大的執行開銷。如果JVM能支持處理器提供的pause指令,那么效率會有一定的提升。pause指令有兩個作用:第一,它可以延遲流水線執行指令,使CPU不會消耗過多的執行資源,延遲的時間取決于具體實現的版本,在一些處理器上延遲時間是零;第二,它可以避免在退出循環的時候因內存順序沖突而引起CPU流水線被清空,從而提高CPU的執行效率。
? ?3)只能保證一個共享變量的原子操作。當對一個共享變量執行操作時,我們可以使用循環CAS的方式來保證原子操作,但是對多個共享變量操作時,循環CAS就無法保證操作的原子性,這個時候就可以用鎖。還有一個巧取的辦法,就是把多個共享變量合并成一個共享變量來操作。比如,有兩個共享變量i=2,j=a,合并一下ij=2a,然后用CAS來操作ij=2a,然后用CAS來操作ij。從JDK1.5開始提供了AtomicReference類來保證引用對象之間的原子性,就可以把多個變量放在一個對象里來進行CAS操作。
? ? ??6.使用鎖機制實現原子操作
? ? ?鎖機制保證了只有獲得鎖的線程才能夠操作鎖定的內存區域。JVM內部實現了很多種鎖機制,有偏向鎖、輕量級鎖和互斥鎖。有意思的是除了偏向鎖,JVM實現鎖的方式都用了循環CAS,即當一個線程想進入同步塊的時候使用循環CAS的方式來獲取鎖,當他退出同步塊的時候使用循環CAS釋放鎖。