java原子操做的實現原理--轉載

原文地址:http://www.infoq.com/cn/articles/atomic-operationjava

1. 引言

原子(atom)本意是「不能被進一步分割的最小粒子」,而原子操做(atomic operation)意爲"不可被中斷的一個或一系列操做" 。在多處理器上實現原子操做就變得有點複雜。本文讓咱們一塊兒來聊一聊在Intel處理器和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)併發

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

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

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。

   
public class Counter {
    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++;
    }
}

在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) //更新後的標誌
    
  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。從Java1.5開始JDK提供了AtomicReference類來保證引用對象之間的原子性,你能夠把多個變量放在一個對象裏來進行CAS操做。

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

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

5. 參考資料

  1. Java SE1.6中的Synchronized
  2. Intel 64和IA-32架構軟件開發人員手冊
  3. 深刻分析Volatile的實現原理

做者介紹

方騰飛,花名清英,淘寶資深開發工程師,關注併發編程,目前在廣告技術部從事無線廣告聯盟的開發和設計工做。我的博客:http://ifeve.com 微博:http://weibo.com/kirals 歡迎經過個人微博進行技術交流。

相關文章
相關標籤/搜索