1. 程式人生 > 實用技巧 >【2020面試】- CAS機制與自旋鎖

【2020面試】- CAS機制與自旋鎖

CAS機制與自旋鎖

CAS(Compare-and-Swap),即比較並替換,java併發包中許多Atomic的類的底層原理都是CAS。

它的功能是判斷記憶體中某個地址的值是否為預期值,如果是就改變成新值,整個過程具有原子性。

具體體現於sun.misc.Unsafe類中的native方法,呼叫這些native方法,JVM會幫我們實現彙編指令,這些指令是CPU的原子指令,因此具有原子性。

public class CASDemo {
  public static void main(String[] args) {

    //初始值5
    AtomicInteger atomicInteger = new AtomicInteger(5);

    //和5比較,設定為10
    System.out.println("預期值:5,當前值:"+atomicInteger);
    System.out.println("是否設定成功:"+atomicInteger.compareAndSet(5, 10));
    //和5比較,設定為15
    System.out.println("預期值:5,當前值:"+atomicInteger);
    System.out.println("是否設定成功:"+atomicInteger.compareAndSet(5, 15));

    System.out.println("當前值:"+atomicInteger);
  }
}

輸出為:

預期值:5,當前值:5
是否設定成功:true
預期值:5,當前值:10
是否設定成功:false
當前值:10

下面看一下getAndAddInt在底層Unsafe類中的程式碼(自旋鎖),運用到了CAS

//va1為物件,var2為地址值,var4是要增加的值,var5為當前地址中最新的值
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; }

首先通過volatile的可見性,取出當前地址中的值,作為期望值。如果期望值與實際值不符,就一直迴圈獲取期望值,直到set成功。

適用場景:

  1. CAS 適合簡單物件的操作,比如布林值、整型值等;

  2. CAS 適合衝突較少的情況,如果太多執行緒在同時自旋,那麼長時間迴圈會導致 CPU 開銷很大;

CAS的缺點:

  1. CPU開銷過大 : 在併發量比較高的情況下,如果許多執行緒反覆嘗試更新某一個變數,卻又一直更新不成功,迴圈往復,會給CPU帶來很到的壓力。

  2. 不能保證程式碼塊的原子性:CAS機制所保證的知識一個變數的原子性操作,而不能保證整個程式碼塊的原子性。比如需要保證3個變數共同進行原子性的更新,就不得不使用synchronized了。

  3.ABA問題:如果記憶體地址V初次讀取的值是A,在CAS等待期間它的值曾經被改成了B,後來又被改回為A,那CAS操作就會誤認為它從來沒有被改變過。

ABA問題以及解決:使用帶版本號的原子引用AtomicStampedRefence<V>,或者叫時間戳的原子引用,類似於樂觀鎖。

// ABA問題及解決方式
public class ABADemo {

  private static AtomicReference<String> atomicReference = new AtomicReference<>("A");
  private static AtomicStampedReference<String> stampReference = new AtomicStampedReference<>("A",1);

  public static void main(String[] args){
    new Thread(()->{
    //獲取到版本號
    int stamp = stampReference.getStamp();
    System.out.println("t1獲取到的版本號:"+stamp);
    try {
      //暫停1秒,確保t1,t2版本號相同
      TimeUnit.SECONDS.sleep(1);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    atomicReference.compareAndSet("A","B");
    atomicReference.compareAndSet("B","A");

    stampReference.compareAndSet("A","B",stamp,stamp+1);
    stampReference.compareAndSet("B","A",stamp+1,stamp+2);
    System.out.println("t1執行緒ABA之後的版本號:"+stampReference.getStamp());

   },"t1").start();

  new Thread(()->{
    //獲取到版本號
    int stamp = stampReference.getStamp();
    System.out.println("t2獲取到的版本號:"+stamp);
    try {
      //暫停2秒,等待t1執行完成ABA
      TimeUnit.SECONDS.sleep(2);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    System.out.print("普通原子類無法解決ABA問題: ");
    System.out.println(atomicReference.compareAndSet("A","C")+"\t"+atomicReference.get());
    System.out.print("版本號的原子類解決ABA問題: ");
    System.out.println(stampReference.compareAndSet("A","C",stamp,stamp+1)+"\t"+stampReference.getReference());

    },"t2").start();
  }
}

輸出結果:普通原子引用類在另一個執行緒完成ABA之後繼續修改(把A改成了C),帶版本號原子引用有效的解決了這個問題。

t1獲取到的版本號:1
t2獲取到的版本號:1
t1執行緒ABA之後的版本號:3
普通原子類無法解決ABA問題: true    C
版本號的原子類解決ABA問題: false    A