原子操作類(二)原子操作的實現原理


文章轉載自 聊聊並發(五)——原子操作的實現原理

原子(atom)本意是“不能被進一步分割的最小粒子”,而原子操作(atomic operation)意為”不可被中斷的一個或一系列操作” 。在多處理器上實現原子操作就變得有點復雜。本文讓我們一起來聊一聊在Inter處理器和Java里是如何實現原子操作的。

2    術語定義

術語名稱 英文 解釋
緩存行 Cache line 緩存的最小操作單位
比較並交換 Compare and Swap CAS操作需要輸入兩個數值,一個舊值(期望操作前的值)和一個新值,在操作期間先比較下在舊值有沒有發生變化,如果沒有發生變化,才交換成新值,發生了變化則不交換。
CPU流水線 CPU pipeline CPU流水線的工作方式就象工業生產上的裝配流水線,在CPU中由5~6個不同功能的電路單元組成一條指令處理流水線,然后將一條X86指令分成5~6步后再由這些電路單元分別執行,這樣就能實現在一個CPU時鍾周期完成一條指令,因此提高CPU的運算速度。
內存順序沖突 Memory order violation 內存順序沖突一般是由假共享引起,假共享是指多個CPU同時修改同一個緩存行的不同部分而引起其中一個CPU的操作無效,當出現這個內存順序沖突時,CPU必須清空流水線。

3    處理器如何實現原子操作

32位IA-32處理器使用基於對緩存加鎖或總線加鎖的方式來實現多處理器之間的原子操作。

3.1   處理器自動保證基本內存操作的原子性

首先處理器會自動保證基本的內存操作的原子性。處理器保證從系統內存當中讀取或者寫入一個字節是原子的,意思是當一個處理器讀取一個字節時,其他處理器不能訪問這個字節的內存地址。奔騰6和最新的處理器能自動保證單處理器對同一個緩存行里進行16/32/64位的操作是原子的,但是復雜的內存操作處理器不能自動保證其原子性,比如跨總線寬度,跨多個緩存行,跨頁表的訪問。但是處理器提供總線鎖定和緩存鎖定兩個機制來保證復雜內存操作的原子性。

3.2   使用總線鎖保證原子性

第一個機制是通過總線鎖保證原子性。如果多個處理器同時對共享變量進行讀改寫(i++就是經典的讀改寫操作)操作,那么共享變量就會被多個處理器同時進行操作,這樣讀改寫操作就不是原子的,操作完之后共享變量的值會和期望的不一致,舉個例子:如果i=1,我們進行兩次i++操作,我們期望的結果是3,但是有可能結果是2。如下圖
1

(例1)

原因是有可能多個處理器同時從各自的緩存中讀取變量i,分別進行加一操作,然后分別寫入系統內存當中。那么想要保證讀改寫共享變量的操作是原子的,就必須保證CPU1讀改寫共享變量的時候,CPU2不能操作緩存了該共享變量內存地址的緩存。

處理器使用總線鎖就是來解決這個問題的。所謂總線鎖就是使用處理器提供的一個LOCK#信號,當一個處理器在總線上輸出此信號時,其他處理器的請求將被阻塞住,那么該處理器可以獨占使用共享內存。

3.3 使用緩存鎖保證原子性

第二個機制是通過緩存鎖定保證原子性。在同一時刻我們只需保證對某個內存地址的操作是原子性即可,但總線鎖定把CPU和內存之間通信鎖住了,這使得鎖定期間,其他處理器不能操作其他內存地址的數據,所以總線鎖定的開銷比較大,最近的處理器在某些場合下使用緩存鎖定代替總線鎖定來進行優化。

頻繁使用的內存會緩存在處理器的L1,L2和L3高速緩存里,那么原子操作就可以直接在處理器內部緩存中進行,並不需要聲明總線鎖,在奔騰6和最近的處理器中可以使用“緩存鎖定”的方式來實現復雜的原子性。所謂“緩存鎖定”就是如果緩存在處理器緩存行中內存區域在LOCK操作期間被鎖定,當它執行鎖操作回寫內存時,處理器不在總線上聲言LOCK#信號,而是修改內部的內存地址,並允許它的緩存一致性機制來保證操作的原子性,因為緩存一致性機制會阻止同時修改被兩個以上處理器緩存的內存區域數據,當其他處理器回寫已被鎖定的緩存行的數據時會起緩存行無效,在例1中,當CPU1修改緩存行中的i時使用緩存鎖定,那么CPU2就不能同時緩存了i的緩存行。

但是有兩種情況下處理器不會使用緩存鎖定。第一種情況是:當操作的數據不能被緩存在處理器內部,或操作的數據跨多個緩存行(cache line),則處理器會調用總線鎖定。第二種情況是:有些處理器不支持緩存鎖定。對於Inter486和奔騰處理器,就算鎖定的內存區域在處理器的緩存行中也會調用總線鎖定。

以上兩個機制我們可以通過Inter處理器提供了很多LOCK前綴的指令來實現。比如位測試和修改指令BTS,BTR,BTC,交換指令XADD,CMPXCHG和其他一些操作數和邏輯指令,比如ADD(加),OR(或)等,被這些指令操作的內存區域就會加鎖,導致其他處理器不能同時訪問它。

4    JAVA如何實現原子操作

在java中可以通過循環CAS的方式來實現原子操作。

4.1 使用循環CAS實現原子操作

JVM中的CAS操作正是利用了上一節中提到的處理器提供的CMPXCHG指令實現的。自旋CAS實現的基本思路就是循環進行CAS操作直到成功為止,以下代碼實現了一個基於CAS線程安全的計數器方法safeCount和一個非線程安全的計數器count。

       private AtomicInteger atomicI = new AtomicInteger(0);

       private int i = 0;

       public static void main(String[] args) {

              final Counter cas = new Counter();

              List<Thread> ts = new ArrayList<Thread>(600);

              long start = System.currentTimeMillis();

              for (int j = 0; j < 100; j++) {

                     Thread t = new Thread(new Runnable() {

                            @Override

                            public void run() {

                                   for (int i = 0; i < 10000; i++) {

                                          cas.count();

                                          cas.safeCount();

                                   }

                            }

                     });

                     ts.add(t);

              }

              for (Thread t : ts) {

                     t.start();

              }

       // 等待所有線程執行完成

              for (Thread t : ts) {

                     try {

                            t.join();

                     } catch (InterruptedException e) {

                            e.printStackTrace();

                     }

              }

              System.out.println(cas.i);

              System.out.println(cas.atomicI.get());

              System.out.println(System.currentTimeMillis() - start);

       }

       /**

        * 使用CAS實現線程安全計數器

        */

       private void safeCount() {

              for (;;) {

                     int i = atomicI.get();

                     boolean suc = atomicI.compareAndSet(i, ++i);

                     if (suc) {

                            break;

                     }

              }

       }

       /**

        * 非線程安全計數器

        */

       private void count() {

              i++;

       }

}

從Java1.5開始JDK的並發包里提供了一些類來支持原子操作,如AtomicBoolean(用原子方式更新的 boolean 值),AtomicInteger(用原子方式更新的 int 值),AtomicLong(用原子方式更新的 long 值),這些原子包裝類還提供了有用的工具方法,比如以原子的方式將當前值自增1和自減1。

在Java並發包中有一些並發框架也使用了自旋CAS的方式來實現原子操作,比如LinkedTransferQueue類的Xfer方法。CAS雖然很高效的解決原子操作,但是CAS仍然存在三大問題。ABA問題,循環時間長開銷大和只能保證一個共享變量的原子操作。

  1. ABA問題。因為CAS需要在操作值的時候檢查下值有沒有發生變化,如果沒有發生變化則更新,但是如果一個值原來是A,變成了B,又變成了A,那么使用CAS進行檢查時會發現它的值沒有發生變化,但是實際上卻變化了。ABA問題的解決思路就是使用版本號。在變量前面追加上版本號,每次變量更新的時候把版本號加一,那么A-B-A 就會變成1A-2B-3A。

從Java1.5開始JDK的atomic包里提供了一個類AtomicStampedReference來解決ABA問題。這個類的compareAndSet方法作用是首先檢查當前引用是否等於預期引用,並且當前標志是否等於預期標志,如果全部相等,則以原子方式將該引用和該標志的值設置為給定的更新值。

public boolean compareAndSet(
               V      expectedReference,//預期引用

               V      newReference,//更新后的引用

              int    expectedStamp, //預期標志

              int    newStamp //更新后的標志
) 
  1. 循環時間長開銷大。自旋CAS如果長時間不成功,會給CPU帶來非常大的執行開銷。如果JVM能支持處理器提供的pause指令那么效率會有一定的提升,pause指令有兩個作用,第一它可以延遲流水線執行指令(de-pipeline),使CPU不會消耗過多的執行資源,延遲的時間取決於具體實現的版本,在一些處理器上延遲時間是零。第二它可以避免在退出循環的時候因內存順序沖突(memory order violation)而引起CPU流水線被清空(CPU pipeline flush),從而提高CPU的執行效率。
  2. 只能保證一個共享變量的原子操作。當對一個共享變量執行操作時,我們可以使用循環CAS的方式來保證原子操作,但是對多個共享變量操作時,循環CAS就無法保證操作的原子性,這個時候就可以用鎖,或者有一個取巧的辦法,就是把多個共享變量合並成一個共享變量來操作。比如有兩個共享變量i=2,j=a,合並一下ij=2a,然后用CAS來操作ij。從Java1.5開始JDK提供了AtomicReference類來保證引用對象之間的原子性,你可以把多個變量放在一個對象里來進行CAS操作。

4.2 使用鎖機制實現原子操作

鎖機制保證了只有獲得鎖的線程能夠操作鎖定的內存區域。JVM內部實現了很多種鎖機制,有偏向鎖,輕量級鎖和互斥鎖,有意思的是除了偏向鎖,JVM實現鎖的方式都用到的循環CAS,當一個線程想進入同步塊的時候使用循環CAS的方式來獲取鎖,當它退出同步塊的時候使用循環CAS釋放鎖。詳細說明可以參見文章Java SE1.6中的Synchronized


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM