說到CAS(CompareAndSwap),不得不先說一說悲觀鎖和樂觀鎖,由於CAS是樂觀鎖思想的一種實現。java
悲觀鎖:老是很悲觀的認爲,每次拿數據都會有其餘線程併發執行,因此每次都會進行加鎖,用完以後釋放鎖,其餘的線程才能拿到鎖,進而拿到資源進行操做。java中的synchronized和ReentrantLock等獨佔鎖就是悲觀鎖思想的實現。安全
樂觀鎖:老是很樂觀認爲,本身拿到數據操做的時候,沒有其餘線程來併發操做,等本身操做結束要更新數據時,判斷本身對數據操做的期間有沒有其餘線程進行操做,若是有,則進行重試,直到操做變動成功。樂觀鎖常使用CAS和版本號機制來實現。java中java.util.atomic包下的原子類都是基於CAS實現的。多線程
CAS指CompareAndSwap,顧名思義,先比較後交換。比較什麼?交換什麼呢?併發
CAS中有三個變量:內存地址V,期待值A, 更新值B。ide
當且僅當內存地址V對應的值與期待值A時相等時,將內存地址V對應的值更換爲B。函數
有了悲觀鎖,樂觀鎖的知識,讓咱們走進java.util.atomic包,看一看java中CAS的實現。測試
這就是java.util.atomic包下的類,咱們着重看AtomicInteger源碼(其餘的都是同樣的思想實現的)this
而後思考CAS有什麼弊端?如何解決弊端?有什麼優缺點?atom
public class AtomicInteger extends Number implements java.io.Serializable { private static final long serialVersionUID = 6214790243416807050L; // 使用Unsafe.compareAndSwapInt進行原子更新操做 private static final Unsafe unsafe = Unsafe.getUnsafe(); //value對應的存儲地址偏移量 private static final long valueOffset; static { try { //使用反射及unsafe.objectFieldOffset拿到value字段的內存地址偏移量,這個值是固定不變的 valueOffset = unsafe.objectFieldOffset (AtomicInteger.class.getDeclaredField("value")); } catch (Exception ex) { throw new Error(ex); } } //volatile修飾的共享變量 private volatile int value; //.......... }
上面的代碼其實就是爲了初始化內存值對應的內存地址偏移量valueOffset,方便後續執行CAS操做時使用。由於這個值一旦初始化,就不會更改,因此使用static final 修飾。線程
咱們能夠看到value使用了volatile修飾,其中也說了volatile的語義。
咱們都知道若是進行value++操做,併發下是不安全的。上一篇中咱們也經過例子證實了volatile只能保證可見性,不能保證原子性。由於value++自己不是原子操做,value++分了三步,先拿到value的值,進行+1,再賦值回value。
咱們先看一看AtomicInteger提供的CAS操做。
/** * 原子地將value設置爲update,若是valueOffset對應的值與expect相等時 * * @param expect 期待值 * @param update 更新值 * @return 若是更新成功,返回true;在valueOffset對應的值與expect不相等時返回false */ public final boolean compareAndSet(int expect, int update) { return unsafe.compareAndSwapInt(this, valueOffset, expect, update); }
咱們已經知道CAS的原理,那來看看下面的測試。你知道輸出的結果是多少嗎?評論區給出你的答案吧。
public class AtomicIntegerTest { public static void main(String[] args) { AtomicInteger atomicInteger = new AtomicInteger(); atomicInteger.compareAndSet(0, 1); atomicInteger.compareAndSet(2, 1); atomicInteger.compareAndSet(1, 3); atomicInteger.compareAndSet(2, 4); System.out.println(atomicInteger.get()); } }
Unsafe提供了三個原子更新的方法。
關於Unsafe類,由於java不支持直接操做底層硬件資源,如分配內存等。若是你使用unsafe開闢的內存,是不被JVM垃圾回收管理,須要本身管理,容易形成內存泄漏等。
咱們上面說了,value++不是原子操做,不能在併發下使用。咱們來看看AtomicInteger提供的原子++操做。
/** * 原子地對value進行+1操做 * * @return 返回更新後的值 */ public final int incrementAndGet() { return unsafe.getAndAddInt(this, valueOffset, 1) + 1; } /** * unsafe提供的方法 * var1 更改的目標對象 * var2 目標對象的共享字段對應的內存地址偏移量valueOffset * var4 須要在原value上增長的值 * @return 返回未更新前的值 */ public final int getAndAddInt(Object var1, long var2, int var4) { //期待值 int var5; do { //獲取valueOffset對應的value的值,支持volatile load var5 = this.getIntVolatile(var1, var2); //若是原子更新失敗,則一直重試,直到成功。 } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); return var5; }
咱們看到CAS只能原子的更新一個值,若是咱們要原子更新多個值,CAS能夠作到嗎?答案是能夠的。
若是要原子地更新多個值,就須要使用AtomicReference。其使用的是compareAndSwapObject方法。能夠將多個值封裝到一個對象中,原子地更換對象來實現原子更新多個值。
public class MultiValue { private int value1; private long value2; private Integer value3; public MultiValue(int value1, long value2, Integer value3) { this.value1 = value1; this.value2 = value2; this.value3 = value3; } } public class AtomicReferenceTest { public static void main(String[] args) { MultiValue multiValue1 = new MultiValue(1, 1, 1); MultiValue multiValue2 = new MultiValue(2, 2, 2); MultiValue multiValue3 = new MultiValue(3, 3, 3); AtomicReference<MultiValue> atomicReference = new AtomicReference<>(); //由於構造AtomicReference時,沒有使用有參構造函數,因此value默認值是null atomicReference.compareAndSet(null, multiValue1); System.out.println(atomicReference.get()); atomicReference.compareAndSet(multiValue1, multiValue2); System.out.println(atomicReference.get()); atomicReference.compareAndSet(multiValue2, multiValue3); System.out.println(atomicReference.get()); } } //輸出結果 //MultiValue{value1=1, value2=1, value3=1} //MultiValue{value1=2, value2=2, value3=2} //MultiValue{value1=3, value2=3, value3=3}
咱們再看一看AtomicReference的compareAndSet方法。
注意:這裏的比較都是使用==而非equals方法。因此最好封裝的MultiValue不要提供set方法。
public final boolean compareAndSet(V expect, V update) { return unsafe.compareAndSwapObject(this, valueOffset, expect, update); }
假設你的帳戶上有100塊錢,你要給女票轉50塊錢。
咱們使用CAS進行原子更新帳戶餘額。因爲某種緣由,你第一次點擊轉帳出現錯誤,你覺得沒有發起轉帳請求,這時候你又點擊了一次。系統開啓了兩個線程進行轉帳操做,第一個線程進行CAS比較,發現你的帳戶上預期是100塊錢,實際也有100塊錢,這時候轉走了50,須要設置爲100 - 50 = 50 元,這時帳戶餘額爲50
第一個線程操做成功了,第二個線程因爲某種緣由阻塞住了;這時候,你的家人又給你轉了50塊錢,而且轉帳成功。那你帳戶上如今又是100塊錢;
太巧了,第二個線程被喚醒了,發現你的帳戶是100塊錢,跟預期的100是相等的,這時候又CAS爲50。大兄弟,哭慘了,你算算,正確的場景你要有多少錢?這就是CAS存在的ABA問題。
public class AtomicIntegerABA { private static AtomicInteger atomicInteger = new AtomicInteger(100); public static void main(String[] args) { ExecutorService executorService = Executors.newFixedThreadPool(3); //線程1 executorService.execute(() -> { System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get()); atomicInteger.compareAndSet(100, 50); System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get()); }); //線程2 executorService.execute(() -> { try { TimeUnit.MILLISECONDS.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get()); atomicInteger.compareAndSet(50, 100); System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get()); }); //線程3 executorService.execute(() -> { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get()); atomicInteger.compareAndSet(100, 50); System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get()); }); executorService.shutdown(); } } //輸出結果 //pool-1-thread-1 - 100 //pool-1-thread-1 - 50 //pool-1-thread-2 - 50 //pool-1-thread-2 - 100 //pool-1-thread-3 - 100 //pool-1-thread-3 - 50
你們心想,靠,這不是坑嗎?那還用。。。。。。。。。。。。。。冷靜,冷靜。你能想到的問題,jdk都能想到。atomic包提供了一個AtomicStampedReference
看名字是否是跟AtomicReference很像啊,其實就是在AtomicReference上加上了一個版本號,每次操做都對版本號進行自增,那每次CAS不只要比較value,還要比較stamp,當且僅當二者都相等,纔可以進行更新。
public AtomicStampedReference(V initialRef, int initialStamp) { pair = Pair.of(initialRef, initialStamp); } //定義了內部靜態內部類Pair,將構造函數初始化的值與版本號構造一個Pair對象。 private static class Pair<T> { final T reference; final int stamp; private Pair(T reference, int stamp) { this.reference = reference; this.stamp = stamp; } static <T> Pair<T> of(T reference, int stamp) { return new Pair<T>(reference, stamp); } } //因此咱們以前的value就對應爲如今的pair private volatile Pair<V> pair;
讓咱們來看一看它的CAS方法。
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))); } private boolean casPair(Pair<V> cmp, Pair<V> val) { return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val); }
仍是上面轉帳的例子,咱們使用AtomicStampedReference來看看是否解決了呢。
public class AtomicStampedReferenceABA { /** * 初始化帳戶中有100塊錢,版本號對應0 */ private static AtomicStampedReference<Integer> atomicInteger = new AtomicStampedReference<>(100, 0); public static void main(String[] args) { ExecutorService executorService = Executors.newFixedThreadPool(3); int[] result = new int[1]; //線程1 executorService.execute(() -> { System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get(result)); //將100更新爲50,版本號+1 atomicInteger.compareAndSet(100, 50, 0, 1); System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get(result)); }); //線程2 executorService.execute(() -> { try { TimeUnit.MILLISECONDS.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get(result)); //將50更新爲100,版本號+1 atomicInteger.compareAndSet(50, 100, 1, 2); System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get(result)); }); //線程3 executorService.execute(() -> { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get(result)); //此線程仍是覺得沒有其餘線程進行過更改,因此舊版本號仍是0 atomicInteger.compareAndSet(100, 50, 0, 1); System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get(result)); }); executorService.shutdown(); } } //輸出結果 //pool-1-thread-1 - 100 //pool-1-thread-1 - 50 //pool-1-thread-2 - 50 //pool-1-thread-2 - 100 //pool-1-thread-3 - 100 //pool-1-thread-3 - 100
媽媽不再用擔憂個人錢少了。
本篇詳細講解了CAS的原理,CAS能夠進行原子更新一個值(包括對象),主要用於讀多寫少的場景,如原子自增操做,若是多線程調用,在CAS失敗以後,會死循環一直重試,直到更新成功。這種狀況是很耗CPU資源的,雖然沒有鎖,但循環的自旋可能比鎖的代價還高。同時存在ABA問題,但AtomicStampedReference經過加入版本號機制已經解決。其實對於atomic包,jdk1.8新增的LongAdder,效率比AtomicLong高,9龍還未涉足,之後確定會品一品。J.U.C(java.util.concurrent)包中大量使用了CAS,ConcurrentHashMap也使用到,若是不瞭解CAS,怎麼入手J.U.C呢。