Java多線程高併發(一) CAS原理

一 問題引入

       當我們測試多個線程操作a++的時候,會出現以下結果

public class CasDemo2 {
    public static void main(String[] args) {
        Castest castest=new Castest();
        for(int i=0;i<10;i++){
                Thread thread=new Thread(castest);
                thread.start();

        }
    }

}

class Castest implements Runnable{

    private   int a=0;
    @Override
    public void run() {
        try {
            Thread.sleep(2000);
            System.out.println(Thread.currentThread().getName()+":"+a++);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

結果並沒有按照我們想要的輸出,我們將a++這個操作其實可以細分成三個步驟:

(1)從內存中讀取a

(2)對a進行加1操作

(3)將a的值重新寫入內存中

發現6出現了三次,說明線程3,9,4獲取a的時候,並不是最新的a。此時你肯定會想到volitile關鍵字,private  volatile int a=0;

但是結果還是會出現這種情況,可以自行嘗試下。我們都知道 volitile具有內存可見性,即線程Avolatile變量的修改,其他線程獲取的volatile變量都是最新的。但是volatile只提供了保證訪問該變量時,每次都是從內存中讀取最新值,並不會使用寄存器緩存該值——每次都會從內存中讀取。而對該變量的修改,volatile並不提供原子性的保證。後續會對volitile進行詳細的說明。

此時當然可以通過synchronized來保證線程安全性

class Castest implements Runnable{

    private volatile  int a=0;
    @Override

    public void run() {
        try {
            synchronized (this){
                Thread.sleep(2000);
                System.out.println(Thread.currentThread().getName()+":"+a++);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

private 輸出結果:

synchronized是一種獨佔鎖,也叫悲觀鎖會導致其它所有需要鎖的線程掛起,等待持有鎖的線程釋放鎖。而另一個更加有效的鎖就是樂觀鎖。所謂樂觀鎖就是,每次不加鎖而是假設沒有衝突而去完成某項操作,如果因爲衝突失敗就重試,直到成功爲止。樂觀鎖用到的機制就是CAS,Compare and Swap。

二 jdk的cas實現

我們可以通過jdk源碼來分析cas的原理, 首先針對上面的線程安全性,是如何通過cas實現的呢,在jdk1.5之後有個Atomic包,可以通過該包下面的方法實現

public class CasDemo2 {
    public static void main(String[] args) {
        Castest castest=new Castest();
        for(int i=0;i<10;i++){
            Thread thread=new Thread(castest);
            thread.start();

        }
    }

}

class Castest implements Runnable{

   AtomicInteger count = new AtomicInteger();

    @Override
    public void run() {
        try {
            System.out.println(Thread.currentThread().getName()+":"+count.getAndIncrement());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

   測試輸出結果:

  我們可以看到結果,在多線程併發情況下,並沒有出現重複值的情況,每個線程拿到的都是不重複的值。看到這裏有些人可能會有疑惑,atomic輸出並沒有像synchronized那樣按順序輸出,爲什麼說是保證線程安全性。你可能對線程安全性有個誤解,

所謂的線程安全性說簡單點就是保證數據的正確性,跟順序關係不大,要想保證線程按順序執行方法很多,比如 線程的wait方法,join方法,wait方法,加鎖等等。舉個生活中的例子,比如a,b,c三人去購物,某個商品的庫存只有10件,不論a,b,c誰先買,庫存的邏輯正確性不會變,a買了2個,那麼b,c只能有8件可以買。跟誰先買後買沒關係,但是一定要保證這個操作的正確性。

三 分析cas源碼

點擊AtomicInteger的getAndIncrement方法,可以看到如下

    /**
     * Atomically increments by one the current value.
     *
     * @return the previous value
     */
    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }
    public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            //獲得給定對象的指定偏移量offset的int值,使用volatile語義,總能獲取到最新的int值。
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }

可以看到主要兩個方法,getIntVolatile與compareAndSwapInt方法,這兩個方法很好的實現了線程安全性

保證原子性的策略:

1:變量都是用Volatile關鍵字修飾。來保證內存可見性(getIntVolatile)

2:使用CAS算法,來保證原子性。(compareAndSwapInt)

關於volatile更好的說明可以查看這邊博客:java內存模型以及volatile

Cas算法源碼:

public final native boolean compareAndSwapInt(
        Object var1,//操作的對象a
        long var2,//對象a的地址偏移量
        int var4,//對象a的期望值
        int var5 //對象a的實際值 
);

這個方法是native,調用C++層JVM的源碼。這裏有JVM的實現源碼下載

鏈接:https://pan.baidu.com/s/1wRVNciNbT7ABGTPbR8Qlqw 
提取碼:lekq 
Unsafe:

    public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

Unsafe.cpp:

UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
  UnsafeWrapper("Unsafe_CompareAndSwapInt");
  oop p = JNIHandles::resolve(obj);
  jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
  return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END

核心方法就是cmpxchg(含義:compare and exchange)

由於這個有多個系統的實現,這裏只看linux_x86架構

atomic_linux_x86.inline.hpp

inline jint     Atomic::cmpxchg    (jint     exchange_value, volatile jint*     dest, jint     compare_value) {
  int mp = os::is_MP();
  __asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)"
                    : "=a" (exchange_value)
                    : "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
                    : "cc", "memory");
  return exchange_value;
}

這裏使用了底層彙編語言,LOCK_IF_MP命令:根據當前系統是否爲多核處理器決定是否爲cmpxchg指令添加lock前綴。

lock的功能:

保證指令的執行的原子性
帶有lock前綴的指令在執行期間會鎖住總線,使得其他處理器暫時無法通過總線訪問內存。很顯然,這會帶來昂貴的開銷。從Pentium 4,Intel Xeon及P6處理器開始,intel在原有總線鎖的基礎上做了一個很有意義的優化:如果要訪問的內存區域(area of memory)在lock前綴指令執行期間已經在處理器內部的緩存中被鎖定(即包含該內存區域的緩存行當前處於獨佔或以修改狀態),並且該內存區域被完全包含在單個緩存行(cache line)中,那麼處理器將直接執行該指令。由於在指令執行期間該緩存行會一直被鎖定,其它處理器無法讀/寫該指令要訪問的內存區域,因此能保證指令執行的原子性。
禁止該指令與之前和之後的讀和寫指令重排序
在AI-32架構軟件開發者手冊第8中內存排序中,有說明LOCK前綴會禁止指令與之前和之後的讀和寫指令重排序。這相當於JMM中定義的StoreLoad內存屏障的效果。也正是因爲這個內存屏障的效果,會使得線程把其寫緩衝區中的所有數據刷新到內存中。注意,這裏不是單單被修改的數據會被回寫到主內存,而是寫緩存中所有的數據都回寫到主內存。
而將寫緩衝區的數據回寫到內存時,就會通過緩存一致性協議(如,MESI協議)和窺探技術來保證寫入的數據被其他處理器的緩存可見。
而這就相當於實現了volatile的內存語義。是的,上面我們爲說明的lock前綴是如何實現volatile的內存語義就是這麼保證的。

cmpxchgl %1,(%3)"
                    : "=a" (exchange_value)
                    : "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
                    : "cc", "memory

cmpxchgl的詳細執行過程:
首先,輸入是"r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp),表示compare_value存入eax寄存器,而exchange_value、dest、mp的值存入任意的通用寄存器。嵌入式彙編規定把輸出和輸入寄存器按統一順序編號,順序是從輸出寄存器序列從左到右從上到下以“%0”開始,分別記爲%0、%1···%9。也就是說,輸出的eax是%0,輸入的exchange_value、compare_value、dest、mp分別是%1、%2、%3、%4。
因此,cmpxchgl %1,(%3)實際上表示cmpxchgl exchange_value,(dest),此處(dest)表示dest地址所存的值。需要注意的是cmpxchgl有個隱含操作數eax,其實際過程是先比較eax的值(也就是compare_value)和dest地址所存的值是否相等,如果相等則把exchange_value的值寫入dest指向的地址。如果不相等則把dest地址所存的值存入eax中。
輸出是"=a" (exchange_value),表示把eax中存的值寫入exchange_value變量中。
Atomic::cmpxchg這個函數最終返回值是exchange_value,也就是說,如果cmpxchgl執行時compare_value和dest指針指向內存值相等則會使得dest指針指向內存值變成exchange_value,最終eax存的compare_value賦值給了exchange_value變量,即函數最終返回的值是原先的compare_value。此時Unsafe_CompareAndSwapInt的返回值(jint)(Atomic::cmpxchg(x, addr, e)) == e就是true,表明CAS成功。如果cmpxchgl執行時compare_value和(dest)不等則會把當前dest指針指向內存的值寫入eax,最終輸出時賦值給exchange_value變量作爲返回值,導致(jint)(Atomic::cmpxchg(x, addr, e)) == e得到false,表明CAS失敗。

四 cas原理

CAS算法圖解

CAS具體執行時,當且僅當預期值A符合內存地址V中存儲的值時,就用新值U替換掉舊值,並寫入到內存地址V中。否則不做更新。

CAS會有如下三個方面的問題:

1.ABA問題,一個線程將內存值從A改爲B,另一個線程又從B改回到A。

2.循環時間長開銷大:CAS算法需要不斷地自旋來讀取最新的內存值,長時間讀取不到就會造成不必要的CPU開銷。
3. 只能保證一個共享變量的原子操作(jdk的AtomicReference來保證應用對象之間的原子性,可以把多個變量放在一個對象裏來進行CAS操作,解決了這一問題)。

ABA問題圖解

ABA問題解決方案:在變量前面添加版本號,每次變量更新的時候都將版本號加1,比如juc的原子包中的AtomicStampedReference類。

 

參考資料:https://www.cnblogs.com/wildwolf0/p/11455796.html

               https://www.jianshu.com/p/bd68ddf91240

   

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章