1. 程式人生 > 其它 >JDK成長記17:Atomic類的原理—CAS+valotile

JDK成長記17:Atomic類的原理—CAS+valotile

經過volatile和synchronize關鍵字的底層原理的洗禮,不知道你是否有一種感覺,要想弄明白它們的原理是一個非常難的過程。為什麼費這麼大的力氣要弄明白這些併發基礎知識呢?

其實是為了之後更好的掌握併發元件、併發集合這些內容。JDK中的juc(併發包)的知識大體可以分為如下幾塊:

併發基礎中除了volatile、synchronied、執行緒狀態變化之外,還有很重要的兩個知識CAS和AQS。而其他併發元件和集合都是基於這些知識來實現的工具而已。

這一節我們通過Atomic類來學習下它的底層原理,實際它的底層通過CAS+volatile的原理來實現的。我們來具體看看:

  • JDK中的CAS如何實現的?又是如何應用在Atomic中的?
  • CAS的自旋效能問題怎麼優化?
  • CAS的ABA問題怎麼解決?

在Java程式碼中,CAS是如何應用在Atomic中的?

在Java程式碼中,CAS是如何應用在Atomic中的?

之前學習synchronized的時候,就接觸過CAS。CAS一般稱作Compare And Swap操作。操作流程如下:

上面CAS操作簡單描述可以如下:當更新一個值從E->V時,更新的時候需要讀取E最新的值N,如果發生了變化,也就是當E!=N,就不會更新成功,重新嘗試,否則更新值成功,變為V。

來看一個Demo:

private static AtomicInteger j = new AtomicInteger(0);

public static void main(String[] args) {
  for (int i = 0; i < 10; i++) {
    new Thread(() -> {
      System.out.println(AtomicIntegerDemo.j.incrementAndGet());
    }).start();
  }
}

這段程式有10個執行緒,它們可能同時更新J的值。但是輸出結果是按順序輸出了0到10的數字。這是因為在AtomicInteger底層,每個執行緒的更新都是CAS操作,它保證了多執行緒修改同一個值的原子性。

首先你可以先看一下AtomicInteger 整體脈絡:

根據之前分析原始碼的思想,先脈絡後細節,你應該可以看出,它核心脈絡就是一個Unsafe物件,int的value。如下

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;

}

核心變數:

  • value是一個volatile的int值。通過volatile的可見性,可以保證有一個執行緒更新了,其他執行緒可以得到最新值。
  • valueOffset,類初始化的時候,來進行執行的,valueOffset,value這個欄位在AtomicInteger這個類中的偏移量,在底層,這個類是有自己對應的結構的,無論是在磁碟的.class檔案裡,還是在JVM記憶體中。大概可以理解為:value這個欄位具體是在AtomicInteger這個類的哪個位置,offset,偏移量,這個是很底層的操作,是通過unsafe來實現的。剛剛在類初始化的時候,就會完成這個操作的,final的,一旦初始化完畢,就不會再變更了。
  • Usafe類,進行真正CAS操作的類

當通過new AtomicInteger(0)這句話建立的物件,實際是給value賦值了一個初始值0而已。(int預設就是0)

 public AtomicInteger(int initialValue) {
    value = initialValue;
  }

現在你可以得到AtomicInteger的核心元件圖如下:

接著每個執行緒會呼叫incrementAndGet這個方法:

  public final int incrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
  }

可以看到直接呼叫了Unsafe的getAndAddInt方法,如下:

  public final int getAndAddInt(Object o, long offset, int delta) {
    int v;
    do {
      v = getIntVolatile(o, offset);
    } while (!compareAndSwapInt(o, offset, v, v + delta));
    return v;
  }

(可以下載OpenJDK原始碼或訪問http://hg.openjdk.java.net/jdk/jdk/file/a1ee9743f4ee/jdk/src/share/classes/sun/misc/Unsafe.java)

上面這段程式碼從脈絡上可以看出來,是經典的CAS操作,首先通過一次volatile讀,讀到最新的v值,之後再通過compareAndSwapInt這個方法,進行CAS操作。如果CAS操作失敗,會返回false,while迴圈繼續執行,進行自旋,重新嘗試CAS操作。如果CAS更新操作成功,返回原值。

返回原值之後,incrementAndGet進行了+1操作,incrementAndGet方法返回,就會的得到更新後的值。

如下圖所示:

在Java程式碼層面,很多Atomic原子類底層核心的原理就是CAS,有人也把這種操作稱為無鎖化,樂觀鎖,自旋鎖之類的。(稱呼這種東西是用來交流的,大家能明白就好,不要太過較真)。

CAS操作每次嘗試修改的時候,就對比一下,有沒有人修改過這個值,沒有人修改,自己就修改,如果有人修改過,就重新查出來最新的值,再次重複那個過程。

而AtomicLong、AtomicLongArray、AtomicBoolean這些原子操作類和AtomicIneger底層是類似的,都是通過Unsafe類來做到CAS操作的。

也正是這種操作,可以保證多執行緒更新的時候的原子性,如下圖所示:

在JVM中的CAS如何實現的?

在JVM中的CAS如何實現的?

具體的CAS操作方法都是native的方法,是通過C++的程式碼實現的。

  public native int   getIntVolatile(Object o, long offset);
  
  public final native boolean compareAndSwapInt(Object o, long offset,
                         int expected,
                         int x);

這就需要我們進一步深入JVM來看下CAS是如何實現的。我給大家找到了對應的C++程式碼如下:

// 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

通過觀察,可以發現,核心的CAS操作是通過Atomic::cmpxchg(x, addr, e)這個方法實現的。這個方法從名字上cmpxchg就可以猜到是compare and exchange和compare and swap(CAS)是一個意思。

但是你繼續跟蹤程式碼會發現,這個Atomic::cmpxchg方法有很多實現。如下圖右側:

因為不同的作業系統和CPU對應的CAS指令可能有所不同,所以有了不同的CAS實現。

這裡可以看一下atomic_linux_x86.inline.hpp 93行的這個實現方法。程式碼如下:

#define LOCK_IF_MP(mp) "cmp $0, " #mp "; je 1f; lock; 1: "

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;
}

上面這段程式碼其實就是CAS最底層的實現方式。

首先通過is_MP 這個判斷表示是否是多核CPU處理的意思(Multi Processor ) 。

接著如果是的話,asm表示彙編指令,傳送一條原子性的彙編指令。也就是說CAS底層實際通過類似於lock cmpxchgl 這種彙編的指令,通知CPU進行原子性的更新。這其實一個輕量的loc指令,可以讓CPU保證原子性的操作,所以說CAS是自旋鎖,是有道理的。

好了,到這裡CAS在JVM層面的具體實現你應該已經瞭解了。接下來我們來看CAS的產生的兩個問題。

CAS的自旋無限迴圈效能問題怎麼優化?

CAS的自旋無限迴圈效能問題怎麼優化?

不知道大家想到過沒,CAS操作有一個很大的問題,如果在高併發,多執行緒更新的時候,會造成大量執行緒進行自旋,消耗CPU資源。這樣的效能是很不好的(當然比synchronized還是要好很多的)。

為了解決這一個問題,Java 8提供的一個對AtomicLong改進後的一個類,LongAdder。它具備一個分段CAS的原子操作類。讓我們來看一下它的分段CAS優化的思想

什麼叫分段CAS,或者說是分段遷移呢?

意思就是,當某一個執行緒如果對一個值更新是,可以看對這個值進行分段更新,每一段叫做一個Cell,在更新每一個Cell的時候,發現說出現了很難更新它的值,出現了多次 CAS失敗了,自旋的時候,進行自動遷移段,它會去嘗試更新別的分段Cell的值,這樣的話就可以讓一個執行緒不會盲目的CAS自旋等待一個更新分段cell的值。

LongAdder正是基於這個思想來實現的。基本原理如下圖所示:

LongAdder先嚐試一次cas更新,如果失敗會轉而通過Cell[]的方式更新值,如果計算index的方式足夠雜湊,那麼在併發量大的情況下,多個執行緒定位到同一個cell的概率也就越低,這有點類似於分段鎖的意思。

但是要注意的是sum方法在併發情況下sum的值不精確,reset方法不是執行緒安全的。

  public void reset() {
    Cell[] as = cells; Cell a;
    base = 0L;
   if (as != null) {
      for (int i = 0; i < as.length; ++i) {
        if ((a = as[i]) != null)
          a.value = 0L;
      }
    }
  }


  public long sum() {
    Cell[] as = cells; Cell a;
    long sum = base;
    if (as != null) {
      for (int i = 0; i < as.length; ++i) {
        if ((a = as[i]) != null)
          sum += a.value;
      }
    }
    return sum;

  }

關於sum不精確

sum方法的實現很簡單,其實就是 base + sum(cells)。由上述原始碼可以發現,sum執行時,最終返回的是sum區域性變數,初始被複製為base,而最終返回時,很可能base已經被更新了,而此時區域性變數sum不會更新,造成不一致。其次,這裡對cell的讀取也無法保證是最後一次寫入的值。

所以,sum方法在沒有併發的情況下,可以獲得正確的結果。

關於reset不執行緒安全

因為reset方法並不是原子操作,它先將base重置成0,假設此時CPU切走執行sum,此時的sum就變成了減去base後的值。也就是說,在併發量大的情況下,執行完此方法並不能保證其他執行緒看到的是重置後的結果。所以要慎用。

在實際工作中,可根據LongAdder和AtomicLong的特點來使用這兩個工具。 當需要在高併發下有較好的效能表現,且對值的精確度要求不高時,可以使用LongAdder(例如網站訪問人數計數)。 當需要保證執行緒安全,可允許一些效能損耗,要求高精度時,可以使用AtomicLong。LongAdder,替代AtomicLong,完全可以對心跳計數器來使用LongAdder。

CAS的ABA問題怎麼解決?

CAS的ABA問題怎麼解決?

最後我們聊一下CAS產生的ABA問題。

什麼是ABA問題?

如果某個值一開始是A,後來變成了B,然後又變成了A,你本來期望的是值如果是第一個A才會設定新值,結果第二個A一比較也ok,也設定了新值,跟期望是不符合的。

怎麼解決呢?

解決辦法也很簡單:加一個類似於版本號的東西,比如郵戳int stamp之類的。記錄更新的次數即可,比較的時候不光比較value也要比較郵戳。

所以atomic包裡有AtomicStampedReference類,就是會比較兩個值的引用是否一致,如果一致,才會設定新值

你可以自己研究一下它的的原始碼。它CAS核心程式碼如下:

  // AtomicStampedReference
  public boolean compareAndSet(V  expectedReference,
                 V  newReference,
                 int expectedStamp,
                 int newStamp) {

    Pair<V> current = pair;
    return
      expectedReference == current.reference &&
      expectedStamp == current.stamp &&
      ((newReference == current.reference &&
       newStamp == current.stamp) ||
       casPair(current, Pair.of(newReference, newStamp)));
  }

  private boolean casPair(Pair<V> cmp, Pair<V> val) {
    return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
  }

最後還要說明的一點,就是Atomic類不能保證多變數原子問題,一般的AtomicInteger,只能保證一個變數的原子性。

但是如果多個變數?你可以用AtomicReference,這個是封裝自定義物件的,多個變數可以放一個自定義物件裡,然後他會檢查這個物件的引用是不是一個。(注意物件中的變數如果不是Atomic,操作的時候不保證原子性,只能保證操作AtomicReference泛型對應的這個物件的引用時是原子的。)

小結

小結

今天,瞭解Atomic的類。這裡給大家小結下Atomic類底層原理都是CAS,原理都是類似的。主要分為如下幾類:

1、AtomicInteger/AtomicLong/AtomicBoolean/AtomicReference是關於對變數的原子CAS更新。

2、AtomicIntegerArray/AtomicLongArray/AtomicReferenceArray是關於對陣列的原子CAS更新。

3、AtomicIntegerFieldUpdater/AtomicLongFieldUpdater/AtomicReferenceFieldUpdater<T,V>是基於反射的CAS原子更新某個類的欄位。

學完這一節你應該掌握瞭如下知識

  • CAS的java層面原理(Unsafe+volatile value)
  • CAS的JVM層面(C++/CPU彙編指令lock cmpxchgl字首指令)
  • ABA問題、無限迴圈效能問題、多變數原子更新問題
  • 分段遷移CAS思想、郵戳版本號思想、原子引用思想

本文由部落格群發一文多發等運營工具平臺 OpenWrite 釋出