Java編髮編程之原子操做與CAS原理分析

引子

以前的文章咱們簡單介紹了線程安全的三個核心概念可見性和有序性和原子性, 那麼這篇文章咱們就來分析一下原子性操做的實現原理java

原子操做

原子本意爲不可分割的最小粒子,而原子操做則爲不可中斷的一個或者系列操做c++

注意的是對一部分操做保持了原子性並不意味着就不會發生線程安全問題, 而是要保證整個臨界區都是原子性的。編程

下面咱們來分析一下cpu和java中如何實現原子操做緩存

Cpu實現

CPU使用基於緩存加鎖或者總線加鎖實現多個CPU的原子性操做安全

cpu自動保證基本內存操做的原子性 處理器保證從系統內存當中讀取或者寫入一個字節是原子的,意思是當一個處理器讀取一個字節時,其餘處理器不能訪問這個字節的內存地址,可是複雜的內存操做處理器不能自動保證其原子性,好比跨總線寬度,跨多個緩存行,跨頁表的訪問。 可是處理器提供總線鎖定和緩存鎖定兩個機制來保證複雜內存操做的原子性多線程

  • 總線加鎖使同時只有一個cpu能獨佔內存進行操做

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

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

  • 緩存加鎖 控制指定cpu緩存達到緩存一致性來防止同時修改相同的緩存數據

總線加鎖存在的問題是在同一時刻咱們只需保證對某個內存地址的操做是原子性便可,但總線鎖定把CPU和內存之間通訊鎖住了,這使得鎖按期間,其餘處理器不能操做其餘內存地址的數據,因此總線鎖定的開銷比較大,如今比較新的處理器在大多數時候使用緩存鎖定代替總線鎖定來進行優化,從而下降鎖的粒度 頻繁使用的內存會緩存在處理器的L1,L2和L3高速緩存裏,那麼原子操做就能夠直接在處理器內部緩存中進行,並不須要聲明總線鎖。 所謂緩存加鎖就是若是緩存在處理器緩存行中的內容在LOCK操做期間被鎖定,當它執行鎖操做回寫內存時,處理器不在總線上聲言LOCK#信號,而是修改內部的內存地址,並容許它的緩存一致性機制來保證操做的原子性,由於緩存一致性機制會阻止同時修改被兩個以上處理器緩存的內存區域數據,當其餘處理器回寫已被鎖定的緩存行的數據時會起緩存行無效。this

有兩種狀況下處理器不會使用緩存鎖定。第一種狀況是:當操做的數據不能被緩存在處理器內部,或操做的數據跨多個緩存行,則處理器會調用總線鎖定。第二種狀況是:有些處理器不支持緩存鎖定,就算鎖定的內存區域在處理器的緩存行中也會調用總線鎖定atom

cpu提供了LOCK前綴的指令來實現加鎖機制。好比交換指令XADD,CMPXCHG和其餘一些操做數和邏輯指令,好比ADD,OR等,被這些指令操做的內存區域就會加鎖,致使其餘處理器不能同時訪問它

Java實現

在Java中有些操做中能夠定義爲原子性

  • 對引用變量和大部分基本類型變量(除long和double以外)的讀寫是原子的。
  • 對全部聲明爲volatile的變量(包括long和double變量)的讀寫操做是原子的

Java可以使用鎖和CAS來實現原子操做,CAS既Compare and Swap比較並替換的意思。

CAS

咱們先來分析一段代碼

public int a = 1;
public boolean compareAndSwapInt(int b) {
    if (a == 1) {
        a = b;
        return true;
    }
    return false;
}

試想這段代碼在多線程併發下,會發生什麼?咱們不妨來分析一下:

線程A執行到 a==1,正準備執行a = b時,線程B也正在運行a = b,並在線程A以前把a修改成2;最後線程A又把a修改爲了3。結果就是兩個線程同時修改了變量a,顯然這種結果是沒法符合預期的,沒法肯定a的值。 解決方法也很簡單,在compareAndSwapInt方法加鎖同步,變成一個原子操做,同一時刻只有一個線程才能修改變量a。

CAS中的比較和替換是一組原子操做,不會被外部打斷,先根據獲取到內存當中當前的內存值,在將內存值和原值做比較,要是相等就修改成要修改的值,屬於硬件級別的操做,效率比加鎖操做高。

JDK中的atomic包的原子操做類都是基於CAS實現的,接下去咱們經過AtomicInteger來看看是如何經過CAS實現原子操做的

public class AtomicInteger extends Number implements java.io.Serializable {
    // setup to use Unsafe.compareAndSwapInt for updates
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    private volatile int value;
    public final int get() {return value;}
}

Unsafe是CAS的核心類,JDK中有一個類Unsafe,它提供了硬件級別的原子操做。 valueOffset表示的是變量值在內存中的偏移地址,由於Unsafe就是根據內存偏移地址獲取數據的原值的。 value是用volatile修飾的,保證了多線程之間看到的value值是同一份。 接下去,咱們看看AtomicInteger是如何實現併發下的累加操做:

public final int getAndAdd(int delta) {    
    return unsafe.getAndAddInt(this, valueOffset, delta);
}

public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
    return var5;
}

其中比較和替換操做放在unsafe類中實現。

若是如今線程A和線程B同時執行getAndAdd操做:

  • AtomicInteger裏面的value原始值爲3,即主內存中AtomicInteger的value爲3,線程A和線程B各自持有一份value的副本,值爲3。
  • 線程A經過getIntVolatile方法獲取到value值3,線程切換,線程A掛起。
  • 線程B經過getIntVolatile方法獲取到value值3,並利用compareAndSwapInt方法比較內存值也爲3,比較成功,修改內存值爲2,線程切換,線程B掛起。
  • 線程A恢復,利用compareAndSwapInt方法比較,發現手裏的值3和內存值2不一致,此時value正在被另一個線程修改,線程A不能修改value值。
  • 線程的compareAndSwapInt實現,循環判斷線程A繼續利用compareAndSwapInt進行比較並替換,直到compareAndSwapInt修改爲功返回true。

咱們再看看Unsafe類中的compareAndSwapInt方法。

public final native boolean compareAndSwapInt(Object paramObject, long paramLong, int paramInt1, int paramInt2);

能夠看到,這是一個本地方法調用,這個本地方法在調用c++代碼,下面是對應於intel X86處理器的源代碼片斷。

inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
    int mp = os::isMP(); //判斷是不是多處理器
    _asm {
        mov edx, dest
        mov ecx, exchange_value
        mov eax, compare_value
        LOCK_IF_MP(mp)
        cmpxchg dword ptr [edx], ecx
    }
}

從上面的源碼中能夠看出,會根據當前處理器類型來決定是否爲cmpxchg指令添加lock前綴。 若是是多處理器,爲cmpxchg指令添加lock前綴,反之,就省略lock前綴。 而lock前綴的做用主要有下面幾點

  • 使用CPU的原子操做確保對內存讀改寫操做的原子執行。
  • 禁止該指令,與前面和後面的讀寫指令重排序。
  • 把寫緩衝區的全部數據刷新到內存中。

CAS存在的問題

CAS雖然很高效的解決原子操做,可是CAS仍然存在三大問題。

  • ABA問題

由於CAS須要在操做值的時候檢查下值有沒有發生變化,若是沒有發生變化則更新,可是若是一個值原來是A,變成了B,又變成了A,那麼使用CAS進行檢查時會發現它的值沒有發生變化,可是實際上卻變化了。ABA問題的解決思路就是使用版本號。在變量前面追加上版本號,每次變量更新的時候把版本號加一,這樣就能區分是否發送了變化

JDK的atomic包裏提供了一個類AtomicStampedReference來解決ABA問題。這個類的compareAndSet方法做用是首先檢查當前引用是否等於預期引用,而且當前標誌是否等於預期標誌,若是所有相等,則以原子方式將該引用和該標誌的值設置爲給定的更新值

  • 循環開銷大,若是自旋不成功就會帶來大量開銷

由於同時修改相同的值的併發比較大就會致使,CAS很難成功,這樣就致使會一直自旋,帶來很大開銷, 因此使用中要避免在併發太大的地方使用

  • 只能保證一個共享變量的原子操做

當對一個共享變量執行操做時,咱們可使用循環CAS的方式來保證原子操做,可是對多個共享變量操做時,循環CAS就沒法保證操做的原子性,有一個取巧的辦法,就是把多個共享變量合併成一個共享變量來操做。JDK提供了AtomicReference類來保證引用對象之間的原子性,你能夠把多個變量放在一個對象裏來進行CAS操做。

固然Java中也能夠經過加鎖來實現原子操做,可是大多數狀況鎖的開銷更大, 接下來的文章咱們會具體分析Java中鎖的實現和原理

總結

本篇文章咱們討論cpu和Java實現原子操做的方式,cpu實現原子操做有總線加鎖和緩存加鎖2種, 而Java主要經過CAS和鎖來實現原子操做,經過分析源碼咱們知道CAS其實是經過lock 指令調用cpu的加鎖方式, 同時咱們討論了CAS會帶來的幾個問題和解決方式,接下來的文章咱們將繼續探討Java併發編程的相關內容。

相關文章
相關標籤/搜索