Java併發/多線程-CAS原理分析

什麼是CAS

CAS 即 compare and swap,比較並交換。

CAS是一種原子操作,同時 CAS 使用樂觀鎖機制。

J.U.C中的很多功能都是建立在 CAS 之上,各種原子類,其底層都用 CAS來實現原子操作。用來解決併發時的安全問題。

併發安全問題

舉一個典型的例子i++

public class AddTest {
  public volatile int i;
  public void add() {
    i++;
  }
}

通過javap -c AddTest可以看到add 方法的字節碼指令:

public void add();
    Code:
       0: aload_0
       1: dup
       2: getfield      #2                  // Field i:I
       5: iconst_1
       6: iadd
       7: putfield      #2                  // Field i:I
      10: return

i++被拆分成了多個指令:

  1. 執行getfield拿到原始內存值;
  2. 執行iadd進行加 1 操作;
  3. 執行putfield寫把累加後的值寫回內存。

假設一種情況:

  • 線程 1 執行到iadd時,由於還沒有執行putfield,這時候並不會刷新主內存區中的值。
  • 此時線程 2 進入開始運行,剛剛將主內存區的值拷貝到私有內存區。
  • 線程 1正好執行putfield,更新主內存區的值,那麼此時線程 2 的副本就是舊的了。錯誤就出現了。

如何解決?

最簡單的,在 add 方法加上 synchronized 。

public class AddTest {
  public volatile int i;
  public synchronized void add() {
    i++;
  }
}

雖然簡單,並且解決了問題,但是性能表現並不好。

最優的解法應該是使用JDK自帶的CAS方案,如上例子,使用AtomicInteger

public class AddIntTest {
  public AtomicInteger i;
  public void add() {
    i.getAndIncrement();
  }
}

底層原理

CAS 的原理並不複雜:

  • 三個參數,一個當前內存值 V、預期值 A、更新值 B
  • 當且僅當預期值 A 和內存值 V 相同時,將內存值修改爲 B 並返回 true
  • 否則什麼都不做,並返回 false

AtomicInteger 類分析,先來看看源碼:

我這裏的環境是Java11,如果是Java8這裏一些內部的一些命名有些許不同。

public class AtomicInteger extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 6214790243416807050L;

    /*
     * This class intended to be implemented using VarHandles, but there
     * are unresolved cyclic startup dependencies.
     */
    private static final jdk.internal.misc.Unsafe U = jdk.internal.misc.Unsafe.getUnsafe();
    private static final long VALUE = U.objectFieldOffset(AtomicInteger.class, "value");

    private volatile int value;
  
		//...
}

Unsafe 類,該類對一般開發而言,少有用到。

Unsafe 類底層是用 C/C++ 實現的,所以它的方式都是被 native 關鍵字修飾過的。

它可以提供硬件級別的原子操作,如獲取某個屬性在內存中的位置、修改對象的字段值。

關鍵點:

  • AtomicInteger 類存儲的值在 value 字段中,而value字段被volatile

  • 在靜態代碼塊中,並且獲取了 Unsafe 實例,獲取了 value 字段在內存中的偏移量 VALUE

接下回到剛剛的例子:

如上,getAndIncrement() 方法底層利用 CAS 技術保證了併發安全。

public final int getAndIncrement() {
  return U.getAndAddInt(this, VALUE, 1);
}

getAndAddInt() 方法:

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

v 通過 getIntVolatile(o, offset)方法獲取,其目的是獲取 ooffset 偏移量的值,其中 o 就是 AtomicInteger 類存儲的值,即valueoffset 內存偏移量的值,即 VALUE

重點weakCompareAndSetInt 就是實現 CAS 的核心方法

  • 如果 ov相等,就證明沒有其他線程改變過這個變量,那麼就把 v 值更新爲 v + delta,其中 delta 是更新的增量值。
  • 反之 CAS 就一直採用自旋的方式繼續進行操作,這一步也是一個原子操作。

分析:

  • 設定 AtomicInteger 的原始值爲 A,線程 1線程 2 各自持有一份副本,值都是 A。
  1. 線程 1 通過getIntVolatile(o, offset)拿到 value 值 A,這時線程 1 被掛起。
  2. 線程 2 也通過getIntVolatile(o, offset)方法獲取到 value 值 A,並執行weakCompareAndSetInt方法比較內存值也爲 A,成功修改內存值爲 B。
  3. 這時線程 1 恢復執行weakCompareAndSetInt方法比較,發現自己手裏的值 A 和內存的值 B 不一致,說明該值已經被其它線程提前修改過了。
  4. 線程 1 重新執行getIntVolatile(o, offset)再次獲取 value 值,因爲變量 value 被 volatile 修飾,具有可見性,線程A繼續執行weakCompareAndSetInt進行比較替換,直到成功

CAS需要注意的問題

使用限制

CAS是由CPU支持的原子操作,其原子性是在硬件層面進行保證的,在Java中普通用戶無法直接使用,只能藉助atomic包下的原子類使用,靈活性受限。

但是CAS只能保證單個變量操作的原子性,當涉及到多個變量時,CAS無能爲力。

原子性也不一定能保證線程安全,如在Java中需要與volatile配合來保證線程安全。

ABA 問題

概念

CAS 有一個問題,舉例子如下:

  • 線程 1 從內存位置 V 取出 A
  • 這時候線程 2 也從內存位置 V 取出 A
  • 此時線程 1 處於掛起狀態,線程 2 將位置 V 的值改成 B,最後再改成 A
  • 這時候線程 1 再執行,發現位置 V 的值沒有變化,符合期望繼續執行。

此時雖然線程 1還是成功了,但是這並不符合我們真實的期望,等於線程 2狸貓換太子線程 1耍了。

這就是所謂的ABA問題

解決方案

引入原子引用,帶版本號的原子操作。

把我們的每一次操作都帶上一個版本號,這樣就可以避免ABA問題的發生。既樂觀鎖的思想。

  • 內存中的值每發生一次變化,版本號都更新。

  • 在進行CAS操作時,比較內存中的值的同時,也會比較版本號,只有當二者都沒有變化時,才能執行成功。

  • Java中的AtomicStampedReference類便是使用版本號來解決ABA問題的。

高競爭下的開銷問題

  • 在併發衝突概率大的高競爭環境下,如果CAS一直失敗,會一直重試,CPU開銷較大。

  • 針對這個問題的一個思路是引入退出機制,如重試次數超過一定閾值後失敗退出。

  • 更重要的是避免在高競爭環境下使用樂觀鎖。


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