天天看點

Java的CAS樂觀鎖原了解析J.U.C包内的原子操作封裝類

CAS全稱 Compare And Swap(比較與交換),在不使用鎖的情況下實作多線程之間的變量同步。屬于硬體同步原語,處理器提供了基本記憶體操作的原子性保證。juc包中的原子類就是通過CAS來實作了樂觀鎖。

CAS算法涉及到三個操作數:

  • 需要讀寫的記憶體值 V。
  • 進行比較的舊值A (期望操作前的值)
  • 要寫入的新值 B。

當且僅當 V 的值等于 A 時,CAS通過原子方式用新值B來更新V的值(“比較+更新”整體是一個原子操作),否則不會執行任何操作。 一般情況下,“更新”是一個不斷重試的過程。

JAVA中的sun.misc.Unsafe類,提供了

  • compareAndSwapInt
  • compareAndSwapLong

等方法實作CAS。

  • 示例
Java的CAS樂觀鎖原了解析J.U.C包内的原子操作封裝類

J.U.C包内的原子操作封裝類

Java的CAS樂觀鎖原了解析J.U.C包内的原子操作封裝類

看一下AtomicInteger的源碼定義:

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

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

複制

各屬性的作用:

  • unsafe: 擷取并操作記憶體的資料
  • valueOffset: 存儲value在AtomicInteger中的偏移
  • value: 存儲AtomicInteger的int值,該屬性需要借助volatile關鍵字保證其線上程間的可見性​​

接着檢視自增方法

incrementAndGet

的源碼時,發現自增函數底層調用的是

unsafe.getAndAddInt

。 但是由于JDK本身隻有Unsafe.class,隻通過class檔案中的參數名,并不能很好的了解方法的作用,是以我們通過OpenJDK 8 來檢視Unsafe的源碼:

// ------------------------- JDK 8 -------------------------
// AtomicInteger 的自增方法
public final int incrementAndGet() {
  return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

// Unsafe.class
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;
}

// ------------------------- OpenJDK 8 -------------------------
// Unsafe.java
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;
}           

複制

由源碼可看出,getAndAddInt()循環擷取給定對象o中的偏移量處的值v,然後判斷記憶體值是否等于v。

  • 如果相等則将記憶體值設定為 v + delta
  • 否則傳回false,繼續循環進行重試,直到設定成功才能退出循環,并且将舊值傳回

整個“比較+更新”操作封裝在compareAndSwapInt()中,通過JNI使用CPU指令完成的,屬于原子操作,可以保證多個線程都能夠看到同一個變量的修改值。

JDK通過CPU的cmpxchg指令,去比較寄存器中的 A 和 記憶體中的值 V。如果相等,就把要寫入的新值 B 存入記憶體中。如果不相等,就将記憶體值 V 指派給寄存器中的值 A。然後通過Java代碼中的while循環再次調用cmpxchg指令進行重試,直到設定成功為止。

CAS的問題

循環+CAS

自旋的實作讓所有線程都處于高頻運作,争搶CPU執行時間的狀态。CAS操作如果長時間不成功,會導緻其一直自旋,如果操作長時間不成功,會帶來很大的CPU資源消耗。

隻能保證一個共享變量的原子操作

對一個共享變量執行操作時,CAS能夠保證原子操作,但是對多個共享變量操作時,CAS是無法保證操作的原子性的。 Java從1.5開始JDK提供了AtomicReference類來保證引用對象之間的原子性,可以把多個變量放在一個對象裡來進行CAS操作。

ABA問題(無法展現資料的變動)

CAS需要在操作值的時候檢查記憶體值是否發生變化,沒有發生變化才會更新記憶體值。但是如果記憶體值原來是A,後來變成了B,然後又變成了A,那麼CAS進行檢查時會發現值沒有發生變化,但是實際上是有變化的。 ABA問題的解決思路就是在變量前面添加版本号,每次變量更新的時候都把版本号加一,這樣變化過程就從“A-B-A”變成了“1A-2B-3A”。

JDK從1.5開始提供了AtomicStampedReference類來解決ABA問題,具體操作封裝在compareAndSet()中。 compareAndSet()首先檢查目前引用和目前标志與預期引用和預期标志是否相等,如果都相等,則以原子方式将引用值和标志的值設定為給定的更新值。 不過目前來說這個類比較”雞肋”,大部分情況下 ABA 問題并不會影響程式并發的正确性,如果需要解決 ABA 問題,使用傳統的互斥同步可能比原子類更加高效。

隻能保證一個共享變量的原子操作。對一個共享變量執行操作時,CAS能夠保證原子操作,但是對多個共享變量操作時,CAS是無法保證操作的原子性的。 Java從1.5開始JDK提供了AtomicReference類來保證引用對象之間的原子性,可以把多個變量放在一個對象裡來進行CAS操作。

Java 8 更新

當然這都是由 Doug Lea 大佬親自為 Java 操刀

更新器

DoubleAccumulator

Java的CAS樂觀鎖原了解析J.U.C包内的原子操作封裝類

LongAccumulator

Java的CAS樂觀鎖原了解析J.U.C包内的原子操作封裝類

計數器

DoubleAdder

Java的CAS樂觀鎖原了解析J.U.C包内的原子操作封裝類

LongAdder

Java的CAS樂觀鎖原了解析J.U.C包内的原子操作封裝類

計數器增強版,高并發下性能更好 頻繁更新但不太頻繁讀取的彙總統計資訊時使用分成多個操作單元,不同線程更新不同的單元

隻有需要彙總的時候才計算所有單元的操作

Java的CAS樂觀鎖原了解析J.U.C包内的原子操作封裝類

T1執行後,A 變成了B

Java的CAS樂觀鎖原了解析J.U.C包内的原子操作封裝類

T3又開始執行了, B變成了A

T2開始執行, A變成了C

  • 問題點:
  • 經曆的A -> B -> A過程,但是對于線程2,無法感覺資料發生了變化
Java的CAS樂觀鎖原了解析J.U.C包内的原子操作封裝類