原子性操作的實現原理
原子的概念:
本意為不能進行分割的最小粒子,原子操作意為不可中斷的一個或者一系列操作
處理器如何實現原子操作
32位IA-32處理器使用基於對快取加鎖或匯流排加鎖的方式來實現多處理器之間的原子操作。首先處理器會自動保證基本的記憶體操作的原子性。處理器保證從系統記憶體中讀取或者寫入一個位元組是原子的,意思是當一個處理器讀取一個位元組時,其他處理器不能訪問這個位元組的記憶體地址。Pentium 6和最新的處理器能自動保證單處理器對同一個快取行裡進行16/32/64位的操作是原子的,但是複雜的記憶體操作處理器是不能自動保證其原子性的,比如跨匯流排寬度、跨多個快取行和跨頁表的訪問。但是,處理器提供匯流排鎖定和快取鎖定兩個機制來保證複雜記憶體操作的原子性。
(1)使用匯流排鎖保證原子性
-第一個機制是通過匯流排鎖保證原子性。如果多個處理器同時對共享變數進行讀改寫操作(i++就是經典的讀改寫操作),那麼共享變數就會被多個處理器同時進行操作,這樣讀改寫操作就不是原子的,操作完之後共享變數的值會和期望的不一致。舉個例子,如果i=1,我們進行兩次i++操作,我們期望的結果是3,但是有可能結果是2,如圖2-3所示。-
原因可能是多個處理器同時從各自的快取中讀取變數i,分別進行加1操作,然後分別寫入系統記憶體中。那麼,想要保證讀改寫共享變數的操作是原子的,就必須保證CPU1讀改寫共享變數的時候,CPU2不能操作快取了該共享變數記憶體地址的快取。
處理器使用匯流排鎖就是來解決這個問題的。所謂匯流排鎖就是使用處理器提供的一個LOCK#訊號,當一個處理器在總線上輸出此訊號時,其他處理器的請求將被阻塞住,那麼該
處理器可以獨佔共享記憶體。
(2)使用快取鎖保證原子性
第二個機制是通過快取鎖定來保證原子性。在同一時刻,我們只需保證對某個記憶體地址的操作是原子性即可,但匯流排鎖定把CPU和記憶體之間的通訊鎖住了,這使得鎖定期間,其他處理器不能操作其他記憶體地址的資料,所以匯流排鎖定的開銷比較大,目前處理器在某些場合下使用快取鎖定代替匯流排鎖定來進行優化。
頻繁使用的記憶體會快取在處理器的L1、L2和L3快取記憶體裡,那麼原子操作就可以直接在處理器內部快取中進行,並不需要宣告匯流排鎖,在Pentium 6和目前的處理器中可以使用“快取鎖定”的方式來實現複雜的原子性。所謂“快取鎖定”是指記憶體區域如果被快取在處理器的快取行中,並且在Lock操作期間被鎖定,那麼當它執行鎖操作回寫到記憶體時,處理器不在總線上聲言LOCK#訊號,而是修改內部的記憶體地址,並允許它的快取一致性機制來保證操作的原子性,因為快取一致性機制會阻止同時修改由兩個以上處理器快取的記憶體區域資料,當其他處理器回寫已被鎖定的快取行的資料時,會使快取行無效,在如圖2-3所示的例子中,當CPU1修改快取行中的i時使用了快取鎖定,那麼CPU2就不能同時快取i的快取行。
但是有兩種情況下處理器不會使用快取鎖定。
第一種情況是:當操作的資料不能被快取在處理器內部,或操作的資料跨多個快取行(cache line)時,則處理器會呼叫匯流排鎖定。
第二種情況是:有些處理器不支援快取鎖定。對於Intel 486和Pentium處理器,就算鎖定的記憶體區域在處理器的快取行中也會呼叫匯流排鎖定。
針對以上兩個機制,我們通過Intel處理器提供了很多Lock字首的指令來實現。例如,位測試和修改指令:BTS、BTR、BTC;交換指令XADD、CMPXCHG,以及其他一些運算元和邏輯指令(如ADD、OR)等,被這些指令操作的記憶體區域就會加鎖,導致其他處理器不能同時訪問它。
3.Java如何實現原子操作
在Java中可以通過鎖和迴圈CAS的方式來實現原子操作。
(1)使用迴圈CAS實現原子操作
JVM中的CAS操作正是利用了處理器提供的CMPXCHG指令實現的。自旋CAS實現的基本思路就是迴圈進行CAS操作直到成功為止,以下程式碼實現了一個基於CAS執行緒安全的計數器方法safeCount和一個非執行緒安全的計數器count。
(2)CAS實現原子操作的三大問題
在Java併發包中有一些併發框架也使用了自旋CAS的方式來實現原子操作,比如LinkedTransferQueue類的Xfer方法。CAS雖然很高效地解決了原子操作,但是CAS仍然存在三大問題。ABA問題,迴圈時間長開銷大,以及只能保證一個共享變數的原子操作。
1)ABA問題。因為CAS需要在操作值的時候,檢查值有沒有發生變化,如果沒有發生變化則更新,但是如果一個值原來是A,變成了B,又變成了A,那麼使用CAS進行檢查時會發現它的值沒有發生變化,但是實際上卻變化了。ABA問題的解決思路就是使用版本號。在變數前面追加上版本號,每次變數更新的時候把版本號加1,那麼A→B→A就會變成1A→2B→3A。從Java 1.5開始,JDK的Atomic包裡提供了一個類AtomicStampedReference來解決ABA問題。這個類的compareAndSet方法的作用是首先檢查當前引用是否等於預期引用,並且檢查當前標誌是否等於預期標誌,如果全部相等,則以原子方式將該引用和該標誌的值設定為給定的更新值。
public boolean compareAndSet(
V expectedReference, // 預期引用
V newReference, // 更新後的引用
int expectedStamp, // 預期標誌
int newSt
)
2)迴圈時間長開銷大。自旋CAS如果長時間不成功,會給CPU帶來非常大的執行開銷。如果JVM能支援處理器提供的pause指令,那麼效率會有一定的提升。pause指令有兩個作用:第一,它可以延遲流水線執行指令(de-pipeline),使CPU不會消耗過多的執行資源,延遲的時間取決於具體實現的版本,在一些處理器上延遲時間是零;第二,它可以避免在退出迴圈的時候因記憶體順序衝突(Memory Order Violation)而引起CPU流水線被清空(CPU Pipeline Flush),從而提高CPU的執行效率。
3)只能保證一個共享變數的原子操作。當對一個共享變數執行操作時,我們可以使用迴圈CAS的方式來保證原子操作,但是對多個共享變數操作時,迴圈CAS就無法保證操作的原子性,這個時候就可以用鎖。還有一個取巧的辦法,就是把多個共享變數合併成一個共享變數來操作。比如,有兩個共享變數i=2,j=a,合併一下ij=2a,然後用CAS來操作ij。從Java 1.5開始,JDK提供了AtomicReference類來保證引用物件之間的原子性,就可以把多個變數放在一個物件裡來進行CAS操作。
(3)使用鎖機制實現原子操作
鎖機制保證了只有獲得鎖的執行緒才能夠操作鎖定的記憶體區域。JVM內部實現了很多種鎖機制,有偏向鎖、輕量級鎖和互斥鎖。有意思的是除了偏向鎖,JVM實現鎖的方式都用了迴圈CAS,即當一個執行緒想進入同步塊的時候使用迴圈CAS的方式來獲取鎖,當它退出同步塊的時候使用迴圈CAS釋放鎖。