CAS機制與自旋鎖

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

它的功能是判斷內存中某個地址的值是否爲預期值,若是是就改變成新值,整個過程具備原子性。多線程

具體體現於sun.misc.Unsafe類中的native方法,調用這些native方法,JVM會幫咱們實現彙編指令,這些指令是CPU的原子指令,所以具備原子性。併發

 1 public class CASDemo {
 2 
 3     public static void main(String[] args) {
 4 
 5         //初始值5
 6         AtomicInteger atomicInteger = new AtomicInteger(5);
 7 
 8         //和5比較,設置爲10
 9         System.out.println("預期值:5,當前值:"+atomicInteger);
10         System.out.println("是否設置成功:"+atomicInteger.compareAndSet(5, 10));
11         //和5比較,設置爲15
12         System.out.println("預期值:5,當前值:"+atomicInteger);
13         System.out.println("是否設置成功:"+atomicInteger.compareAndSet(5, 15));
14 
15         System.out.println("當前值:"+atomicInteger);
16     }
17 }

輸出爲:this

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

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

//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成功。spa

適用場景:線程

  1. CAS 適合簡單對象的操做,好比布爾值、整型值等;code

  2. CAS 適合衝突較少的狀況,若是太多線程在同時自旋,那麼長時間循環會致使 CPU 開銷很大;對象

CAS的缺點:blog

  1. CPU開銷過大 : 在併發量比較高的狀況下,若是許多線程反覆嘗試更新某一個變量,卻又一直更新不成功,循環往復,會給CPU帶來很到的壓力。

  2. 不能保證代碼塊的原子性:CAS機制所保證的知識一個變量的原子性操做,而不能保證整個代碼塊的原子性。好比須要保證3個變量共同進行原子性的更新,就不得不使用synchronized了。

  3.  ABA問題:若是內存地址V初次讀取的值是A,在CAS等待期間它的值曾經被改爲了B,後來又被改回爲A,那CAS操做就會誤認爲它歷來沒有被改變過。

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

 0 // ABA問題及解決方式
1 public class ABADemo { 2 3 private static AtomicReference<String> atomicReference = new AtomicReference<>("A"); 4 private static AtomicStampedReference<String> stampReference = new AtomicStampedReference<>("A",1); 5 6 public static void main(String[] args){ 7 new Thread(()->{ 8 //獲取到版本號 9 int stamp = stampReference.getStamp(); 10 System.out.println("t1獲取到的版本號:"+stamp); 11 try { 12 //暫停1秒,確保t1,t2版本號相同 13 TimeUnit.SECONDS.sleep(1); 14 } catch (InterruptedException e) { 15 e.printStackTrace(); 16 } 17 atomicReference.compareAndSet("A","B"); 18 atomicReference.compareAndSet("B","A"); 19 20 stampReference.compareAndSet("A","B",stamp,stamp+1); 21 stampReference.compareAndSet("B","A",stamp+1,stamp+2); 22 System.out.println("t1線程ABA以後的版本號:"+stampReference.getStamp()); 23 24 },"t1").start(); 25 26 new Thread(()->{ 27 //獲取到版本號 28 int stamp = stampReference.getStamp(); 29 System.out.println("t2獲取到的版本號:"+stamp); 30 try { 31 //暫停2秒,等待t1執行完成ABA 32 TimeUnit.SECONDS.sleep(2); 33 } catch (InterruptedException e) { 34 e.printStackTrace(); 35 } 36 System.out.print("普通原子類沒法解決ABA問題: "); 37 System.out.println(atomicReference.compareAndSet("A","C")+"\t"+atomicReference.get()); 38 System.out.print("版本號的原子類解決ABA問題: "); 39 System.out.println(stampReference.compareAndSet("A","C",stamp,stamp+1)+"\t"+stampReference.getReference()); 40 41 },"t2").start(); 42 } 43 }

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

t1獲取到的版本號:1
t2獲取到的版本號:1
t1線程ABA以後的版本號:3
普通原子類沒法解決ABA問題: true    C
版本號的原子類解決ABA問題: false    A
相關文章
相關標籤/搜索