「Java」手把手理解CAS實現原理

mrchaochao發表於2020-10-15
  • 先來看看概念,【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:表示需要修改為的新值
  • 此處描述一下 偏移量 的概念?

「Java」手把手理解CAS實現原理

 

 這裡的偏移量就像我們【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】問題。

「Java」手把手理解CAS實現原理

 

  • 話不多說,我們這裡用程式碼來模擬一下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)

「Java」手把手理解CAS實現原理

 

  • 我們對上述程式碼改造一下,再看看結果:
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問題!

相關文章