文章目錄
- 概述
- CAS底層原理
- CAS缺點
- ABA問題
- 解決ABA問題
- AtomicStampedReference
- 總結
概述
CAS的全稱是Compare-And-Swap,它是CPU并發原語
它的功能是判斷記憶體某個位置的值是否為預期值,如果是則更改為新的值,這個程序是原子的
CAS并發原語體現在Java語言中就是sun.misc.Unsafe類的各個方法,呼叫UnSafe類中的CAS方法,JVM會幫我們實作出CAS匯編指令,這是一種完全依賴于硬體的功能,通過它實作了原子操作,再次強調,由于CAS是一種系統原語,原語屬于作業系統用于范疇,是由若干條指令組成,用于完成某個功能的一個程序,并且原語的執行必須是連續的,在執行程序中不允許被中斷,也就是說CAS是一條CPU的原子指令,不會造成所謂的資料不一致的問題,也就是說CAS是執行緒安全的,
CAS底層原理
在Volatile中談到有JUC下的原子類,用來執行緒安全的實作number++,那就是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思想: 也就是自旋,自我旋轉
CAS缺點
CAS不加鎖,保證一次性,但是需要多次比較
- 回圈時間長,開銷大(因為執行的是do while,如果比較不成功一直在回圈,最差的情況,就是某個執行緒一直取到的值和預期值都不一樣,這樣就會無限回圈)
- 只能保證一個共享變數的原子操作
- 當對一個共享變數執行操作時,我們可以通過回圈CAS的方式來保證原子操作
- 但是對于多個共享變數操作時,回圈CAS就無法保證操作的原子性,這個時候只能用鎖來保證原子性
- 引出來ABA問題
ABA問題
俗話來說就是“貍貓換太子”,比如十年前的小明同學跟十年后的小明同學,雖然是表明上同一個人,但十年間他發生了什么事你并不清楚,

假設現在有兩個執行緒,分別是T1 和 T2,然后T1執行某個操作的時間為10秒,T2執行某個時間的操作是2秒,最開始AB兩個執行緒,分別從主記憶體中獲取A值,但是因為B的執行速度更快,他先把A的值改成B,然后在修改成A,然后執行完畢,T1執行緒在10秒后,執行完畢,判斷記憶體中的值為A,并且和自己預期的值一樣,它就認為沒有人更改了主記憶體中的值,就快樂的修改成B,但是實際上 可能中間經歷了 ABCDEFA 這個變換,也就是中間的值經歷了貍貓換太子,
所以ABA問題就是,在進行獲取主記憶體值的時候,該記憶體值在我們寫入主記憶體的時候,已經被修改了N次,但是最終又改成原來的值了
CAS只管開頭和結尾,也就是頭和尾是一樣,那就修改成功,中間的這個程序,可能會被人修改過
解決ABA問題
新增一種機制,也就是修改版本號,類似于時間戳的概念
如原來的A,版本號為2,落后于現在的版本號3,所以要重新獲取最新值,這里就提出了一個使用時間戳版本號,來解決ABA問題的思路
這里引出原子應用的概念
原子參考其實和原子包裝類是差不多的概念,就是將一個java類,用原子參考類進行包裝起來,那么這個類就具備了原子性
AtomicStampedReference
時間戳原子參考,來這里應用于版本號的更新,也就是每次更新的時候,需要比較期望值和當前值,以及期望版本號和當前版本號
public class ABADemo {
/**
* 普通的原子參考包裝類
*/
static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
// 傳遞兩個值,一個是初始值,一個是初始版本號
static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100, 1);
public static void main(String[] args) {
System.out.println("============以下是ABA問題的產生==========");
new Thread(() -> {
// 把100 改成 101 然后在改成100,也就是ABA
atomicReference.compareAndSet(100, 101);
atomicReference.compareAndSet(101, 100);
}, "t1").start();
new Thread(() -> {
try {
// 睡眠一秒,保證t1執行緒,完成了ABA操作
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 把100 改成 101 然后在改成100,也就是ABA
System.out.println(atomicReference.compareAndSet(100, 2019) + "\t" + atomicReference.get());
}, "t2").start();
System.out.println("============以下是ABA問題的解決==========");
new Thread(() -> {
// 獲取版本號
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + "\t 第一次版本號" + stamp);
// 暫停t3一秒鐘
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 傳入4個值,期望值,更新值,期望版本號,更新版本號
atomicStampedReference.compareAndSet(100, 101, atomicStampedReference.getStamp(), atomicStampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName() + "\t 第二次版本號" + atomicStampedReference.getStamp());
atomicStampedReference.compareAndSet(101, 100, atomicStampedReference.getStamp(), atomicStampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName() + "\t 第三次版本號" + atomicStampedReference.getStamp());
}, "t3").start();
new Thread(() -> {
// 獲取版本號
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + "\t 第一次版本號" + stamp);
// 暫停t4 3秒鐘,保證t3執行緒也進行一次ABA問題
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean result = atomicStampedReference.compareAndSet(100, 2019, stamp, stamp+1);
System.out.println(Thread.currentThread().getName() + "\t 修改成功否:" + result + "\t 當前最新實際版本號:" + atomicStampedReference.getStamp());
System.out.println(Thread.currentThread().getName() + "\t 當前實際最新值" + atomicStampedReference.getReference());
}, "t4").start();
}
}

執行緒t3,在進行ABA操作后,版本號變更成了3,而執行緒t4在進行操作的時候,就出現操作失敗了,因為版本號和當初拿到的不一樣
總結
CAS
CAS是compareAndSwap,比較當前作業記憶體中的值和主物理記憶體中的值,如果相同則執行規定操作,否者繼續比較直到主記憶體和作業記憶體的值一致為止
CAS應用
CAS有3個運算元,記憶體值V,舊的預期值A,要修改的更新值B,當且僅當預期值A和記憶體值V相同時,將記憶體值V修改為B,否者什么都不做
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/150185.html
標籤:其他
