無鎖同步-JAVA之Volatile、Atomic和CAS

一、概要

      本文是無鎖同步系列文章的第二篇,主要探討JAVA中的原子操做,以及如何進行無鎖同步。html

      關於JAVA中的原子操做,咱們很容易想到的是Volatile變量、java.util.concurrent.atomic包和JVM提供的CAS操做。java

二、Volatile

    1)Volatile變量不具備原子性

           Volatile變量具備一種可見性,該特性能保證不一樣線程甚至處理器核心在對這種類型的變量在讀取的時候能讀到最新的值。但Volatile變量不提供原子操做的保證。緩存

 

            下面咱們給出一個例子:數據結構

 1 public class test {  2   
 3   volatile static int someValue;  4   
 5   public static void main(String[] args) {  6     someValue = 1;  7     int b = someValue;  8  }  9 
10 }

      在這個例子中,咱們對一個int類型的volatile變量進行寫和讀,在這種場景下volatile變量的讀和寫操做是原子的(注意:這裏指是讀操做和寫操做分別是原子的),而且jvm會爲咱們保證happens-before語義(即會保證寫操做在讀操做以前發生,其實jvm給咱們提供的不單止是happens-before,具體詳情咱們在本系列的下一篇博文再具體介紹)。咱們能夠利用這個特性完成一小部分線程同步的需求。可是咱們須要注意下面這種狀況。app

1 public class test { 2   
3   volatile static int someValue; 4   
5   public static void main(String[] args) { 6     someValue++; 7  } 8 
9 }

     在這裏,咱們把讀和寫操做改爲一個自增操做,那麼這個自增操做是否是原子的呢?jvm

     答案是否認的。ide

     自增操做其本質是性能

1 int tmp = someValue; 2 tmp += 1; 3 someValue = tmp;

     這裏包含讀、加、寫3個操做。對於int類型來講,java保證這裏面的讀和寫操做中是原子的,但不保證它們加在一塊兒仍然是原子的。優化

     也正是因爲這個特性,單獨使用volatile變量還不足以實現計數器等包含計算的需求。可是若是使用恰當,這種變量將爲線程間的同步帶來無可比擬的性能提高。this

    2)Volatile變量如何保證可見性

      咱們知道現代的CPU爲了優化性能,計算時通常不與內存直接交互。通常先把數據從內存讀取到CPU內部緩存再進行操做。而不一樣線程可能由不一樣的CPU內核執行,極可能會致使某變量在不一樣的處理器中保存着2個不一樣副本的狀況,致使數據不一致,產生意料以外的結果。那麼java是怎麼保證volatile變量在全部線程中的數據都是一致的呢?

      若對一個Volatile變量進行賦值,編譯後除了生成賦值字節碼外,還會生成一個lock指令。該指令是CPU提供的,能實現下面2個功能:

  1. 將CPU當前緩存行內的新數據寫入內存
  2. 將其它CPU核內心包含本變量的緩存行無效化,以強制下次讀取時到內存中讀取

      上述過程基於CPU內部的一套緩存協議。具體能夠查閱相關文檔。

二、java.util.concurrent.atomic包和CAS

    對比volatile變量,atomic包給咱們提供了AtomicInteger、AtomicLong、AtomicBooleanAtomicReference、 AtomicIntegerArray、 AtomicLongArray、 AtomicReferenceArray等一系列類,提供了相應類型一系列的原子操做。它們的接口語義很是明顯,下面咱們選AtomicInteger加以說明,讀者能夠觸類旁通學會其餘原子類的用法。

      AtomicInteger

          Get()/Set()

          下面咱們進入AtomicInteger類探祕,看看它是如何實現原子讀寫的。(下文使用的源碼均來自JDK7)

 

 1     private volatile int value;  2 
 3     /**  4  * Gets the current value.  5  *  6  * @return the current value  7      */
 8     public final int get() {  9         return value; 10  } 11 
12     /** 13  * Sets to the given value. 14  * 15  * @param newValue the new value 16      */
17     public final void set(int newValue) { 18         value = newValue; 19     }

 

          沒有錯,就是利用咱們上面提到的volatile實現的。

      compareAndSet(int expect, int update)和weakCompareAndSet(int expect, int update)

          這就是著名的CAS(compare and set)接口。

          對比變量的值和expect是否相等,若是相等則將變量的值更新爲update。參考第一篇,咱們能夠根據這個特性實現一些無鎖數據結構。事實上,JDK8中的java.util.concurrent包有很多數據結構被使用CAS優化,其中最著名的就是ConcurrentHashMap。

         而要說到weak版本的CAS接口有什麼特別之處,它的註釋說明它會"fail spuriously",可是其源碼倒是如出一轍的。

 1     /**  2  * Atomically sets the value to the given updated value  3  * if the current value {@code ==} the expected value.  4  *  5  * @param expect the expected value  6  * @param update the new value  7  * @return true if successful. False return indicates that  8  * the actual value was not equal to the expected value.  9      */
10     public final boolean compareAndSet(int expect, int update) { 11         return unsafe.compareAndSwapInt(this, valueOffset, expect, update); 12  } 13 
14     /** 15  * Atomically sets the value to the given updated value 16  * if the current value {@code ==} the expected value. 17  * 18  * <p>May <a href="package-summary.html#Spurious">fail spuriously</a> 19  * and does not provide ordering guarantees, so is only rarely an 20  * appropriate alternative to {@code compareAndSet}. 21  * 22  * @param expect the expected value 23  * @param update the new value 24  * @return true if successful. 25      */
26     public final boolean weakCompareAndSet(int expect, int update) { 27         return unsafe.compareAndSwapInt(this, valueOffset, expect, update); 28     }

          證實SUN JDK 7沒有按照標準實現weak版本的接口,可是咱們沒法保證之後的JDK是如何實現的。所以,不管什麼時候,咱們都不該假定weak版本的CAS操做和非weak版本具備徹底一致的行爲。

      其餘經常使用接口

          int addAndGet(int delta)
          以原子方式將給定值與當前值相加。 功能等價於i=i+delta。

          int getAndAdd(int delta)
          以原子方式將給定值與當前值相加。 功能等價於{int tmp=i;i+=delta;return tmp;}。

          int getAndIncrement()
          以原子方式將當前值加 1。 功能等價於i++。

          int decrementAndGet()
          以原子方式將當前值減 1。 功能等價於--i。

          int getAndDecrement()
          以原子方式將當前值減 1。 功能等價於i--。

          int getAndSet(int newValue)
          以原子方式設置爲給定值,並返回舊值。 功能等價於{int tmp=i;i=newValue;return tmp;}。

          int incrementAndGet()
          以原子方式將當前值加 1。 功能等價於++i。 

 三、CAS的ABA問題

  描述

    ABA問題的描述以下:

 

  1. 進程P1在共享變量中讀到值爲A
  2. P1被搶佔,進程P2得到CPU時間片並執行
  3. P2把共享變量裏的值從A改爲了B,再改回到A
  4. P2被搶佔,進程P1得到CPU時間片並執行
  5. P1回來看到共享變量裏的值沒有被改變,繼續按共享變量沒有被改變的邏輯執行

 

     顯然,這極可能致使不可預料的錯誤。

  JAVA中的解決方案

     在java.util.concurrent.atomic包中,有一個AtomicStampedReference類,它提供了一個帶有Stamp字段的CAS接口。

 1 /**  2  * Atomically sets the value of both the reference and stamp  3  * to the given update values if the  4  * current reference is {@code ==} to the expected reference  5  * and the current stamp is equal to the expected stamp.  6  *  7  * @param expectedReference the expected value of the reference  8  * @param newReference the new value for the reference  9  * @param expectedStamp the expected value of the stamp 10  * @param newStamp the new value for the stamp 11  * @return true if successful 12      */
13     public boolean compareAndSet(V expectedReference, 14  V newReference, 15                                  int expectedStamp, 16                                  int newStamp) { 17         Pair<V> current = pair; 18         return
19             expectedReference == current.reference &&
20             expectedStamp == current.stamp &&
21             ((newReference == current.reference &&
22               newStamp == current.stamp) ||
23  casPair(current, Pair.of(newReference, newStamp))); 24     }

    你們可能已經發現,這個Stamp參數就至關於一個版本號,當版本號和變量的值均一致的時候才容許更新變量。

    咱們試着用這個方法解決ABA問題:

  1. 進程P1在共享變量中讀到值爲A,Stamp爲0。下面咱們用二元組(A, 0)表示共享變量的值
  2. P1被搶佔,進程P2得到CPU時間片並執行
  3. P2把共享變量裏的值從(A, 0)改爲了(B, 1),再嘗試把值修改成A,同時更新Stamp。即改成(A, 2)
  4. P2被搶佔,進程P1得到CPU時間片並執行
  5. P1回來嘗試更新共享變量的值,A在expectedStamp參數傳入原數值0,卻發現如今Stamp已經不是0了,CAS操做失敗
  6. P1知道共享變量已經被改變,避免了BUG出現

    到這裏,ABA問題被解決。

四、總結

    線程同步的方法不少,在適當的場景下靈活運用原子操做,避免使用鎖能夠提升咱們的程序性能。

相關文章
相關標籤/搜索