- 先來看看概念,【CAS】 全稱“CompareAndSwap”,中文翻譯即“比較並替換”。
定義:CAS操作包含三個運算元 —— 記憶體位置(V),期望值(A),和新值(B)。
如果記憶體位置的值與期望值匹配,那麼處理器會自動將該位置值更新為新值。否則,
處理器不作任何操作。無論哪種情況,它都會在CAS指令之前返回該位置的值。
(CAS在一些特殊情況下,僅返回CAS是否成功,而不提去當前值)CAS有效說明了
“我認為【位置V】應該包含【值A】:如果包含【值A】,則將【新值B】放到這個位置;
否則,不要更改該位置的值,只告訴我這個位置現在的值即可”。
- 怎麼使用JDK提供CAS支援?
Java中提供了對CAS操作的支援,具體在【sun.misc.unsafe】類中(官方不建議直接使用)
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5); public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5); public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
引數var1:表示要操作的物件
引數var2:表示要操作物件中屬性地址的偏移量
引數var4:表示需要修改資料的期望的值
引數var5:表示需要修改為的新值
- 此處描述一下偏移量的概念?
這裡的偏移量就像我們【new】一個物件,物件的地址就是【0x001】,那麼value的地址就是【0x002 = 0x001 + 1】, 【+1】就是偏移量。
- CAS的實現原理是什麼?
CAS通過呼叫JNI的程式碼實現(JNI:Java Native Interface),允許java呼叫其他語言,
而【compareAndSwapXXX】系列的方法就是藉助“C語言”來呼叫cpu底層指令實現的。
以常用的【Intel x86】平臺來說,最終對映到cpu的指令為【cmpxchg】(compareAndChange),
這是一個原子指令,cpu執行此命令時,實現比較替換操作。
- 那麼問題來了,現在計算機動不動就上百核,【cmpxchg】怎麼保證多核下的執行緒安全?
系統底層進行CAS操作時,會判斷當前系統是否為多核系統,如果是,就給【匯流排】加鎖,
只有一個執行緒對匯流排加鎖成功, 加鎖成功之後會執行CAS操作,也就是說CAS的原子性是平臺級別的。
- 那麼問題又來了,CAS這麼流批,就不會有什麼問題麼?
1》高併發下,其他執行緒會一直處於自旋阻塞狀態 2》ABA問題(重要)
- 什麼是ABA問題呢?
CAS需要在操作值的時候,檢查下值有沒有發生變化,如果沒有發生變化則更新,
但是可能會有這樣一個情況,如果一個值原來是A,在CAS方法執行之前,被其他執行緒修改為了B,然後又修改回成A,
此時CAS方法執行之前,檢查的時候發現它的值並沒有發生變化,但實際卻變化了,這就是【CAS的ABA】問題。
- 話不多說,我們這裡用程式碼來模擬一下ABA問題:
public class CasABADemo1 { private static AtomicInteger count = new AtomicInteger(0); public static void main(String[] args) { System.out.println("mainThread 當前count值為: " + count.get()); Thread mainThread = new Thread(() -> { try { int expectCount = count.get(); int updateCount = expectCount + 1; System.out.println("mainThread 期望值:" + expectCount + ", 修改值:" + updateCount); Thread.sleep(2000);//休眠2000s ,釋放cpu boolean result = count.compareAndSet(expectCount, updateCount); System.out.println("mainThread 修改count : " + result); } catch (InterruptedException e) { e.printStackTrace(); } }); Thread otherThread = new Thread(() -> { try { Thread.sleep(20);//確保主執行緒先獲取到cpu資源 } catch (InterruptedException e) { e.printStackTrace(); } count.incrementAndGet(); System.out.println("其他執行緒先修改 count 為:" + count.get()); count.decrementAndGet(); System.out.println("其他執行緒又修改 count 為:" + count.get()); }); mainThread.start(); otherThread.start(); } }
結果:
mainThread 當前count值為: 0 mainThread 期望值:0, 修改值:1 其他執行緒先修改 count 為:1 其他執行緒又修改 count 為:0 mainThread 修改count : true
最後結果可以看出【mainThread】修改成功,但是【mainThread】獲取到的【expectCount】雖然也是1,但已經不是曾經的【expectCount】。
- 如何解決ABA問題呢?
解決ABA最簡單的方案就是給值加一個版本號,每次值變化,都會修改他的版本號,
CAS操作時都去對比次版本號。
- java中提供了一種版本號控制的方法,可以解決ABA問題:
public boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp)
- 我們對上述程式碼改造一下,再看看結果:
public class CasABADemo2 { private static AtomicStampedReference<Integer> count = new AtomicStampedReference<>(0, 1); public static void main(String[] args) { System.out.println("mainThread 當前count值為: " + count.getReference() + ",版本號為:" + count.getStamp()); Thread mainThread = new Thread(() -> { try { int expectStamp = count.getStamp(); int updateStamp = expectStamp + 1; int expectCount = count.getReference(); int updateCount = expectCount + 1; System.out.println("mainThread 期望值:" + expectCount + ", 修改值:" + updateCount); Thread.sleep(2000);//休眠2000s ,釋放cpu boolean result = count.compareAndSet(expectCount, updateCount, expectStamp, updateStamp); System.out.println("mainThread 修改count : " + result); } catch (InterruptedException e) { e.printStackTrace(); } }); Thread otherThread = new Thread(() -> { try { Thread.sleep(20);//確保主執行緒先獲取到cpu資源 } catch (InterruptedException e) { e.printStackTrace(); } count.compareAndSet(count.getReference(), count.getReference() + 1, count.getStamp(), count.getStamp() + 1); System.out.println("其他執行緒先修改 count 為:" + count.getReference() + " ,版本號:" + count.getStamp()); count.compareAndSet(count.getReference(), count.getReference() - 1, count.getStamp(), count.getStamp() + 1); System.out.println("其他執行緒又修改 count 為:" + count.getReference() + " ,版本號:" + count.getStamp()); }); mainThread.start(); otherThread.start(); } }
結果:
mainThread 當前count值為: 0,版本號為:1 mainThread 期望值:0, 修改值:1 其他執行緒先修改 count 為:1 ,版本號:2 其他執行緒又修改 count 為:0 ,版本號:3 mainThread 修改count : false
可見新增版本號可以完美的解決ABA問題!