Java的CAS樂觀鎖原理解析

CAS全稱 Compare And Swap(比較與交換),在不使用鎖的狀況下實現多線程之間的變量同步。屬於硬件同步原語,處理器提供了基本內存操做的原子性保證。juc包中的原子類就是經過CAS來實現了樂觀鎖。java

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

  • 須要讀寫的內存值 V。
  • 進行比較的舊值A (指望操做前的值)
  • 要寫入的新值 B。

當且僅當 V 的值等於 A 時,CAS經過原子方式用新值B來更新V的值(「比較+更新」總體是一個原子操做),不然不會執行任何操做。
通常狀況下,「更新」是一個不斷重試的過程。segmentfault

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

  • compareAndSwapInt
  • compareAndSwapLong

等方法實現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。this

  • 若是相等則將內存值設置爲 v + delta
  • 不然返回false,繼續循環進行重試,直到設置成功才能退出循環,而且將舊值返回

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

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

LongAccumulator

計數器

DoubleAdder

LongAdder


計數器加強版,高併發下性能更好
頻繁更新但不太頻繁讀取的彙總統計信息時使用分紅多個操做單元,不一樣線程更新不一樣的單元

只有須要彙總的時候才計算全部單元的操做


T1執行後,A 變成了B

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

T2開始執行, A變成了C

  • 問題點:
  • 經歷的A -> B -> A過程,可是對於線程2,沒法感知數據發生了變化

本文由博客一文多發平臺 OpenWrite 發佈!
相關文章
相關標籤/搜索