一:CAS簡介
CAS:Compare And Swap(字面意思是比較與交換),JUC包中大量使用到了CAS,比如我們的atomic包下的原子類就是基於CAS來實現。區別於悲觀鎖synchronized,CAS是樂觀鎖的一種實現,在某些場合使用它可以提高我們的並發性能。
在CAS中,主要是涉及到三個操作數,所期盼的舊值、當前工作內存中的值、要更新的值,僅當所期盼的舊值等於當前值時,才會去更新新值。
二:CAS舉例
比如當如下場景,由於i++是個復合操作,讀取、自增、賦值三步操作,因此在多線程條件下我們需要保證i++操作的安全
public class CASTest { int i = 0; public void increment() { i++; } }
解決辦法有通過使用synchronized來解決,synchronized解決了並發編程的原子性,可見性,有序性。
public class CASTest { int i = 0; public synchronized void increment() { i++; } }
但synchronized畢竟是悲觀鎖,盡管它后續進行了若干優化,引入了鎖的膨脹升級措施,但是還是存在膨脹為重量級鎖而導致阻塞問題,因此,我們可以使用基於CAS實現的原子類AtomicInteger來保證其原子性
public class CASTest { AtomicInteger i = new AtomicInteger(0); public static void increment() { //自增並返回新值 i.incrementAndGet(); } }
三:CAS原理分析
atomic包下的原子類就是基於CAS實現的,我們拿AtomicInteger來分析下CAS.
public class AtomicInteger extends Number implements java.io.Serializable { private static final long serialVersionUID = 6214790243416807050L; // CAS操作是基於一個Unsafe類,Unsafe類是整個Concurrent包的基礎,里面所有的函數都是native的 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); } } //底層采用volatile修飾值,保證其可見性和有序性 private volatile int value;
從AtomicInteger定義的相關屬性來看,其內部的操作都是基於Unsafe類,因為在Java中,我們並不能直接操作內存,但是Java還是開放了一個Unsafe類來給我們進行操作,顧名思義,Unsafe,是不安全的,因此要謹慎使用。
其內部定義的值是用volatiel進行修飾的,volatile可以保證有序性和可見性,具體為什么可以保證就不在此闡述了。
再來看看其幾個核心的API
//以原子方式將值設置為給定的新值 expect:期望值 update:舊值 public final boolean compareAndSet(int expect, int update) { return unsafe.compareAndSwapInt(this, valueOffset, expect, update); } //以原子方式將當前值+1,返回期望值 public final int incrementAndGet() { return unsafe.getAndAddInt(this, valueOffset, 1) + 1; } //以原子方式將當前值-1,返回期望值 public final int decrementAndGet() { return unsafe.getAndAddInt(this, valueOffset, -1) - 1; }
關於其源碼還是很少的,基本都是基於Unsafe類進行實現的。
先來看看compareAndSet方法,其調用的是Unsafe的compareAndSwapInt方法,當工作內存中的值與所期盼的舊值不相同的時候,會更新失敗,舉例說明:
public class CASDemo { public static void main(String[] args) { AtomicInteger atomicInteger = new AtomicInteger(2020); System.out.println("更新結果:"+atomicInteger.compareAndSet(2020, 2021)); System.out.println("當前值為:"+atomicInteger.get()); //自增加一 atomicInteger.getAndIncrement(); System.out.println("更新結果:"+atomicInteger.compareAndSet(2020, 2021)); System.out.println("當前值為:"+atomicInteger.get()); } }
在來看看incrementAndGet方法,其調用的是unsafe.getAndAddInt方法,其就相當於是自旋鎖的實現,當所期盼的舊值與新值相同時才更新成功,否則就進行自旋操作直到更新成功為止。
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; }
四:CAS缺點分析
CAS的優點很明顯,基於樂觀鎖的思想,提高了並發情況下的性能,缺點主要是ABA問題、自旋時間過長導致CPU占有率過高、只能保證一個共享變量的原子性。
ABA問題
就是一個值由A變為B,在由B變為A,使用CAS操作無法感知到該種情況下出現的變化,帶來的后果很嚴重,比如銀行內部員工,從系統挪走一百萬,之后還了回來,系統感知不到豈不是要出事。模擬下出現ABA問題:public class ABA { private static AtomicInteger atomicInteger = new AtomicInteger(0); public static void main(String[] args) { //線程t1實現0->1->0 Thread t1 = new Thread(new Runnable() { @Override public void run() { atomicInteger.compareAndSet(0,1); atomicInteger.compareAndSet(1,0); } },"t1"); //線程t2實現0->100 Thread t2 = new Thread(new Runnable() { @Override public void run() { try { //模擬狸貓換太子行為 TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("更新結果:"+atomicInteger.compareAndSet(0, 100)); } }); t1.start(); t2.start(); } }
運行結果是:true
解決ABA可以使每一次修改都帶上時間戳,以記錄版本號的形式來使的CAS感知到這種狸貓換太子的操作。Java提供了AtomicStampedReference類來解決,該類除了指定舊值與期盼值,還要指定舊的版本號與期盼的版本號
public boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp) { Pair<V> current = pair; return expectedReference == current.reference && expectedStamp == current.stamp && ((newReference == current.reference && newStamp ==current.stamp) || casPair(current, Pair.of(newReference, newStamp))); }
public class ABA_Test { // 初始值100,版本號1 private static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<Integer>(100, 1); public static void main(String[] args) throws InterruptedException { // AtomicStampedReference實現 Thread tsf1 = new Thread(new Runnable() { @Override public void run() { try { // 讓 tsf2先獲取stamp,導致預期時間戳不一致 TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } // 預期引用:100,更新后的引用:110,預期標識getStamp() 更新后的標識getStamp() + 1 atomicStampedReference.compareAndSet(100, 110, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1); atomicStampedReference.compareAndSet(110, 100, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1); } }); Thread tsf2 = new Thread(new Runnable() { @Override public void run() { int stamp = atomicStampedReference.getStamp(); try { TimeUnit.SECONDS.sleep(2); // 線程tsf1執行完 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println( "AtomicStampedReference:" + atomicStampedReference.compareAndSet(100, 120, stamp, stamp + 1)); } }); tsf1.start(); tsf2.start(); } }
運行結果:
自旋次數過長
CAS是基於樂觀鎖的思想實現的,當頻繁出現當前值與所舊預期值不相等的情況,會導致頻繁的自旋而使得浪費CPU資源。
只能保證單個共享變量的原子性
單純對共享變量進行CAS操作,只能保證單個,無法使多個共享變量同時進行原子操作。
參考資料
狂神說Java:www.bilibili.com/video/BV1B7…
CAS機制及AtomicInteger源碼分析:juejin.im/post/5e2182…