cas——compareAndSwap

概念

  • compareAndSwap翻譯過來就是 比較並交換
  • cas底層 調用的是unSafe,unSafed對底層的修改調用的native方法(CPU併發原語),天然原子性

代碼說話

  • 創建一個AtomicInteger類,初始化值5,此時線程A去修改,把5讀到工作內存,修改成2000,在寫回主內存時,會比較當時拿到工作內存的5和現在主內存是否一致, 一致則修改成2000,不一致則表示被其他線程修改了。
  • 底層代碼

  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;
    
    		/**
   * Atomically increments by one the current value.
   *
   * @return the previous value
   */
  public final int getAndIncrement() {
      return unsafe.getAndAddInt(this, valueOffset, 1);
  }
  
  // 這裏會做自旋
  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;
    }

  • 假設線程A和線程B同時執行getAndAddInt操作

    • 1 AtomicInteger裏面的value原始值爲3,即主內存中Atomicinteger的value=3,根據Jmm模型,線程A和線程B各自持有一份值爲3的value的副本拷貝到各自的工作內存。

    • 2 線程A通過getIntVolatile(var1, var2)拿到value值3,這時線程A被掛起。

    • 3 線程B通過getIntVolatile(var1,var2)拿到value值3,此時線程B得到CPU執行權,執行compareAndSwapint方法比較內存值也爲3,成功修改成4 ,此時主內存值爲4了

    • 4 線程A恢復,執行compareSwapint方法比較,發現自己手裏的值3和主內存不一致了,本次修改失敗,重新讀取再來一遍

    • 5 線程A重新獲取value值,因爲變量value被volatile修飾,是可見的,compareSwapint方法成功,修改成功。

UnSafe類的理解

  • UnSafe
    • 它是CAS的核心類,由於java方法無法直接訪問底層系統,需要通過本地方法來訪問,Unsage相當於是一個後門,基於該類可以直接操作特定內存的數據。

    • Unsafe類存在於sun.misc包中,其 內部方法操作可以像C的指針一樣直接操作內存

    • Unsafe類中的所有方法都是native修飾的,也就是說Unsafe類中的方法都直接調用操作系統底層資源執行相應的任務

  • compareAndSwapInt
    • 該方法的實現位於unsafe.cpp中

    • 使用cmpxchg指令比較並更新值(保證原子性),下面是底層寫法

      // obj 是當前工作內存的值
      // offset 是獲取主內存的值
      oop p = JNIHandles::resolve(obj)
      jint* addr = (jint *) index_oop_from_field_offset_long(p, offset)
      (Atomic::cmpxchg(x, addr, e))
      

AtomicInteger是如何保證線程安全

  • 變量valueOffset 標識該變量值在內存中的 偏移地址,因爲Unsafe就是根據內存 偏移地址 來獲取數據的

    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }
      
    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;
    }
    
  • value用volatile修飾,保證了內存之間的可見性

    private volatile int value;
    
  • 總結

    • 利用CAS(底層是靠硬件層面的鎖) + volatile來保證原子性,從而避免synchronized的高開銷,提升執行效率

cas的缺點

  • 當前線程如果每次比較並交換的時候都返回false,就會一直請求,會給CPU帶來很大的開銷
  • ABA問題
    • 當線程A去讀主內存的值爲1, 另一個線程B修改了這個值爲2並寫回主內存,然後又重新修改回1寫回主內存,然後線程A進行修改寫回主內存是可以成功的
    • 儘管cas成功了,但是不代表這個過程是沒問題的
    • 解決:
      • 通過添加版本號的方式
      • 如:AtomicReference會有ABA問題、AtomicStampedReference沒有ABA問題

底層深究

  • CPU併發原語爲什麼就能保證原子性 (借鑑)
    • CPU 處理器速度遠遠大於在主內存中的,爲了解決速度差異,在他們之間架設了多級緩存,如 L1、L2、L3 級別的緩存,這些緩存離CPU越近就越快,將頻繁操作的數據緩存到這裏,加快訪問速度

    • image

    • 現在都是多核 CPU 處理器,每個 CPU 處理器內維護了一塊字節的內存,每個內核內部維護着一塊字節的緩存,當多線程併發讀寫時,就會出現緩存數據不一致的情況

    • 總線鎖定

      • 當一個處理器要操作共享變量時,在 BUS 總線上發出一個 Lock 信號,其他處理就無法操作這個共享變量了。

      • 缺點很明顯,總線鎖定在阻塞其它處理器獲取該共享變量的操作請求時,也可能會導致大量阻塞,從而增加系統的性能開銷。

    • 緩存鎖定

      • 後來的處理器都提供了緩存鎖定機制,也就說當某個處理器對緩存中的共享變量進行了操作,其他處理器會有個嗅探機制,將其他處理器的該共享變量的緩存失效,待其他線程讀取時會重新從主內存中讀取最新的數據,基於 MESI 緩存一致性協議來實現的。

        • MESI是Modified、Exclusive、Shared、Invalid這四個單詞的首字母。這4個字母分別代表4種狀態

          狀態 描述 監聽任務
          Modified 該緩存行有效,但是該緩存數據已經被當前核心修改,此時和DRAM中數據不一致。我們將其置爲M,其他的核中緩存行都會置爲I。 監聽總線上所有對該緩存行寫回DRAM的操作(不希望別人寫入),需要將該操作延遲到自己將緩存行寫回到主存後變成S狀態。
          Excluside(互斥) 該緩存行有效,數據和RAM的數據一致,數據只存在當前內核工作內存中,只有他在使用是獨佔的。 監聽總線上所有從DRAM讀取該緩存行的操作,一旦有讀的,需要將狀態置爲S狀態。
          Shared(共享) 該緩存行有效,不過當前緩存行在多個核中都有,並且大家以及DRAM中的都一樣。 監聽其他的緩存中將該緩存置爲I或者爲E的事件,將狀態置爲I狀態。
          Invalid(無效) 表明該緩存行無效,如果想要獲取數據的話,就去DRAM中加載最新的。 不需要監聽。
      • 現代的處理器基本都支持和使用的緩存鎖定機制

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