CAS底層原理萬字示例+詳解!

概念

CAS的全稱是Compare-And-Swap,它是cpu併發原語

它的功能是判斷內存某個位置的值是否爲預期值。如果是則更改爲新的值,這個過程是原子的CAS併發原語體現在java語言中就是sun.misc.Unsafe類的各個方法。調用UnSafe類中的CAS方法,JVM會幫我們實現出CAS彙編指令,這是一種完全依賴於硬件的功能,通過它實現了原子操作,再次強調,由於CAS是一種系統原語,原語屬於操作系統用於範疇,是由若干條指令組成,用於完成某個功能的一個過程,並且原語的執行必須是連續的,在執行過程中不允許被中斷,也就是說CAS是一條CPU的原子指令,不會造成所謂的數據不一致的問題,也就是說CAS是線程安全的。

代碼使用

首先使用AtomicInteger創建了一個實例,並初始化爲5

// 創建一個原子類
AtomicInteger atomicInteger= new AtomicInteger(5) 

然後調用CAS方法,企圖更新成2019,這裏有兩個參數,一個是5,表示期望值,第二個就是我們要更新的值

atomicInteger.compareAndSet(5,2019) 

然後再次使用了一個方法,同樣將值改爲1024

atomicInteger.compareAndSet(5,1024) 

完整代碼如下:

public class CASDemo {
    public static void main(String[] args) {
        // 創建一個原子類
        AtomicInteger atomicInteger = new AtomicInteger(5);

        /**
         * 一個是期望值,一個是更新值,但期望值和原來的值相同時,才能夠更改
         * 假設三秒前,我拿的是5,也就是expect爲5,然後我需要更新成 2019
         */
        System.out.println(atomicInteger.compareAndSet(5, 2019) + "\t current data: " + atomicInteger.get());

        System.out.println(atomicInteger.compareAndSet(5, 1024) + "\t current data: " + atomicInteger.get());
    }
} 

上面代碼的執行結果爲:

這是因爲我們執行第一個的時候,期望值和原本值是滿足的,因此修改成功,但是第二次後,主內存的值已經改成了2019,不滿足期望值,因此返回了false,本次寫入失敗

這個就類似於SVN或者Git的版本號,如果沒有人更改過,就能夠正常提交,否者需要先將代碼pull下來,合併代碼後,然後提交

CAS底層原理

首先我們先看看atomicInteger.getAndIncrement()方法的源碼

從這裏能夠看到,底層又調用了一個unsafe類的getAndAddInt方法

unsafe類

Unsafe是CAS的核心類,由於Java方法無法直接訪問底層系統,需要通過本地(Native)方法來訪問,Unsafe相當於一個後門,基於該類可以直接操作特定的內存數據,Unsafe類存在sun.misc包中,其內部方法操作可以像C指針一樣直接操作內存,因爲Java中的CAS操作的執行依賴於Unsafe類的方法。

注意Unsafe類的所有方法都是native修飾的,也就是說unsafe類中的方法都直接調用操作系統底層資源執行相應的任務

爲什麼Atomic修飾的包裝類,能夠保證原子性,依靠的就是底層的unsafe類

變量valueOffset

表示該變量值在內存中的偏移地址,因爲Unsafe就是根據內存偏移地址獲取數據的

從這裏我們可以看到,通過valueOffset,直接通過內存地址,獲取到值,然後進行加1操作

變量value用volatile修飾

保證了多線程之間的內存可見性

var5:就是我們從主內存中拷貝到工作內存中的值

那麼操作的時候,需要比較工作內存中的值,和主內存中的值進行比較

假設執行 compareAndSwapInt返回false,那麼就一直執行 while方法,直到期望的值和真實值一樣

  • val1:AtomicInteger對象本身
  • var2:該對象值得引用地址
  • var4:需要變動的數量
  • var5:用var1和var2找到的內存中的真實值
    • 用該對象當前的值與var5比較
    • 如果相同,更新var5 + var4 並返回true
    • 如果不同,繼續取值然後再比較,直到更新完成

這裏沒有用synchronized,而用CAS,這樣提高了併發性,也能夠實現一致性,是因爲每個線程進來後,進入的do while循環,然後不斷的獲取內存中的值,判斷是否爲最新,然後在進行更新操作。

假設線程A和線程B同時執行getAndInt操作(分別跑在不同的CPU上)

  1. AtomicInteger裏面的value原始值爲3,即主內存中AtomicInteger的 value 爲3,根據JMM模型,線程A和線程B各自持有一份價值爲3的副本,分別存儲在各自的工作內存
  2. 線程A通過getIntVolatile(var1 , var2) 拿到value值3,這是線程A被掛起(該線程失去CPU執行權)
  3. 線程B也通過getIntVolatile(var1, var2)方法獲取到value值也是3,此時剛好線程B沒有被掛起,並執行了compareAndSwapInt方法,比較內存的值也是3,成功修改內存值爲4,線程B打完收工,一切OK
  4. 這是線程A恢復,執行CAS方法,比較發現自己手裏的數字3和主內存中的數字4不一致,說明該值已經被其它線程搶先一步修改過了,那麼A線程本次修改失敗,只能夠重新讀取後在來一遍了,也就是在執行do while
  5. 線程A重新獲取value值,因爲變量value被volatile修飾,所以其它線程對它的修改,線程A總能夠看到,線程A繼續執行compareAndSwapInt進行比較替換,直到成功。

Unsafe類 + CAS思想: 也就是自旋,自我旋轉

底層彙編

Unsafe類中的compareAndSwapInt是一個本地方法,該方法的實現位於unsafe.cpp中

  • 先想辦法拿到變量value在內存中的地址
  • 通過Atomic::cmpxchg實現比較替換,其中參數X是即將更新的值,參數e是原內存的值

CAS缺點

CAS不加鎖,保證一次性,但是需要多次比較

  • 循環時間長,開銷大(因爲執行的是do while,如果比較不成功一直在循環,最差的情況,就是某個線程一直取到的值和預期值都不一樣,這樣就會無限循環)
  • 只能保證一個共享變量的原子操作
    • 當對一個共享變量執行操作時,我們可以通過循環CAS的方式來保證原子操作
    • 但是對於多個共享變量操作時,循環CAS就無法保證操作的原子性,這個時候只能用鎖來保證原子性
  • 引出來ABA問題?

ABA問題

假設兩個線程T1和T2訪問同一個變量V,當T1訪問變量V時,讀取到V的值爲A;此時線程T1被搶佔了,T2開始執行,T2先將變量V的值從A變成B,然後又將變量V的值從B變成A;此時T1又搶佔了主動權,繼續執行,它發現V的值還是A,以爲沒有變化,所以就繼續執行了。這個過程中,變量V從A變爲B,再由B變爲A就形象的稱爲ABA問題。
總結
--

CAS

CAS是compareAndSwap,比較當前工作內存中的值和主物理內存中的值,如果相同則執行規定操作,否者繼續比較直到主內存和工作內存的值一致爲止

CAS應用

CAS有3個操作數,內存值V,舊的預期值A,要修改的更新值B。當且僅當預期值A和內存值V相同時,將內存值V修改爲B,否者什麼都不做

補充

參考:https://blog.csdn.net/longgeqiaojie304/article/details/89819316

1、CAS底層是彙編

Unsafe類中的compareAndSwapInt,是一個本地方法,該方法的實現位於unsafe.cpp中

2、CAS會導致ABA問題

CAS算法實現一個重要前提需要取出內存中某個時刻的數據並在當下時刻比較並替換,那麼在這個時間差內會導致數據的變化。

比如說一個線程one從內存位置V中取出A,這個時候另一個線程two也從內存位置V中取出A,並且線程two進行了一些操作將值變成了B,然後線程two又將V位置的數據變成A,這時候線程one進行CAS操作時發現內存中仍然是A,然後線程one操作成功。

儘管線程one的CAS操作成功,但是並不代表這個過程就是沒有問題的。

作者:柒
鏈接:https://www.cnblogs.com/qiuwenli/p/13516505.html
來源:cnblogs

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章