CAS原理解析
CAS底層原理
概念
CAS的全稱是Compare-And-Swap,它是CPU併發原語
它的功能是判斷記憶體某個位置的值是否為預期值,如果是則更改為新的值,這個過程是原子的
CAS併發原語體現在Java語言中就是sun.misc.Unsafe類的各個方法。呼叫UnSafe類中的CAS方法,JVM會幫我們實現出CAS彙編指令,這是一種完全依賴於硬體的功能,通過它實現了原子操作,再次強調,由於CAS是一種系統原語,原語屬於作業系統應用範疇,是由若干條指令組成,用於完成某個功能的一個過程,並且原語的執行必須是連續的,在執行過程中不允許被中斷,也就是說CAS是一條CPU的原子指令,不會造成所謂的資料不一致的問題,也就是說CAS是執行緒安全的。
程式碼使用
首先呼叫AtomicInteger建立了一個例項, 並初始化為10
// 建立一個原子類
AtomicInteger atomicInteger = new AtomicInteger(10);
然後呼叫CAS方法,企圖更新成2020,這裡有兩個引數,一個是10,表示期望值,第二個就是我們要更新的值
atomicInteger.compareAndSet(10, 2020)
然後再次使用了一個方法,同樣將值改成2021
atomicInteger.compareAndSet(10, 2021)
完整程式碼如下:
/** * CAS 比較並交換 * @author xiao * @date 2020/4/23 9:14 */ public class CASDemo { public static void main(String[] args) { AtomicInteger atomicInteger = new AtomicInteger(10); /** * 一個是期望值,一個是更新值,但期望值和原來的值相同時,才能夠更改 * 假設拿的是10,也就是expect為5,然後我需要更新成 2020 */ System.out.println(atomicInteger.compareAndSet(10, 2020)+"\t當前的值為:"+atomicInteger.get()); System.out.println(atomicInteger.compareAndSet(10, 2021)+"\t當前的值為:"+atomicInteger.get()); } }
上面程式碼的執行結果為
這是因為我們執行第一個的時候,期望值和原本值是滿足的,因此修改成功,但是第二次後,主記憶體的值已經修改成了2020,不滿足期望值,因此返回了false,本次寫入失敗
這個就類似於SVN或者Git的版本號,如果沒有人更改過,就能夠正常提交,否者需要先將程式碼pull下來,合併程式碼後,然後提交
CAS底層原理
首先我們先看看 atomicInteger.getAndIncrement()方法的原始碼
從這裡能夠看到,底層又呼叫了一個unsafe類的getAndAddInt方法
1、unsafe類
Unsafe是CAS的核心類,由於Java方法無法直接訪問底層系統,需要通過本地(Native)方法來訪問,Unsafe相當於一個後門,基於該類可以直接操作特定的記憶體資料。Unsafe類存在sun.misc包中,其內部方法操作可以像C的指標一樣直接操作記憶體,因為Java中的CAS操作的執行依賴於Unsafe類的方法。
注意Unsafe類的所有方法都是native修飾的,也就是說unsafe類中的方法都直接呼叫作業系統底層資源執行相應的任務
為什麼Atomic修飾的包裝類,能夠保證原子性,依靠的就是底層的unsafe類
2、變數valueOffset
表示該變數值在記憶體中的偏移地址,因為Unsafe就是根據記憶體偏移地址獲取資料的。
從這裡我們能夠看到,通過valueOffset,直接通過記憶體地址,獲取到值,然後進行加1的操作
3、變數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上)
- AtomicInteger裡面的value原始值為3,即主記憶體中AtomicInteger的 value 為3,根據JMM模型,執行緒A和執行緒B各自持有一份價值為3的副本,分別儲存在各自的工作記憶體
- 執行緒A通過getIntVolatile(var1 , var2) 拿到value值3,這是執行緒A被掛起(該執行緒失去CPU執行權)
- 執行緒B也通過getIntVolatile(var1, var2)方法獲取到value值也是3,此時剛好執行緒B沒有被掛起,並執行了compareAndSwapInt方法,比較記憶體的值也是3,成功修改記憶體值為4,執行緒B打完收工,一切OK
- 這是執行緒A恢復,執行CAS方法,比較發現自己手裡的數字3和主記憶體中的數字4不一致,說明該值已經被其它執行緒搶先一步修改過了,那麼A執行緒本次修改失敗,只能夠重新讀取後在來一遍了,也就是在執行do while
- 執行緒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問題
總結
CAS
CAS是compareAndSwap,比較當前工作記憶體中的值和主實體記憶體中的值,如果相同則執行規定操作,否者繼續比較直到主記憶體和工作記憶體的值一致為止
CAS應用
CAS有3個運算元,記憶體值V,舊的預期值A,要修改的更新值B。當且僅當預期值A和記憶體值V相同時,將記憶體值V修改為B,否則不操