序言
由於最近項目上遇到了高併發問題,而自己對高併發,多線程這裏的知識點相對薄弱,尤其是基礎,所以想系統的學習一下,以後可能會出一系列的JUC文章及總結 ,同時也爲企業級的高併發項目做好準備。
本文是JUC文章的第二篇,如想看以往關於JUC文章,請點擊JUC系列總結
此係列文章的總結思路大致分爲三部分:
- 理論(概念);
- 實踐(代碼證明);
- 總結(心得及適用場景)。
在這裏提前說也是爲了防止大家看着看着就迷路了。
CAS大綱
首先,下圖是本文的大綱,也就是說在看本文之前,你需要先了解本文到底是講什麼內容,有個整體大觀,然後逐個細分到內容層次去講解。
CAS理論
在上一文中(從代碼實踐的角度解析volatile關鍵字),我們介紹了i++的爲什麼不保證原子性以及i++的一些處理辦法,其中有一種方式是採用AtomicInteger類,但是你知道爲什麼麼?
查看
AtomicInteger.getAndIncrement()
方法,發現其沒有加synchronized
也實現了同步。這是爲什麼?
什麼是CAS?
CAS的全稱是Compare-And-Swap,它是一條CPU併發原語。
正如它的名字一樣,比較並交換,它是一種很重要的同步思想。如果主內存的值跟期望值一樣,那麼就進行修改,否則一直重試,直到一致爲止。
而原語的執行必須是連續的,在執行過程中不允許被中斷,也就是說CAS是一條CPU的原子指令,不會造成所謂的數據不一致性問題。
它的功能是判斷內存某個位置的值是否爲預期值,如果是則更改爲新的值,這個過程是原子的。
我們來看一段代碼:
public class CasDemo {
public static void main(String[] args) {
//初始值
AtomicInteger integer = new AtomicInteger(5);
//比較並替換
boolean flag = integer.compareAndSet(5, 10);
boolean flag2 = integer.compareAndSet(5, 15);
System.out.println("是否自選並替換 \t"+flag +"\t更改之後的值爲:"+integer.get());
System.out.println("是否自選並替換 \t"+flag2 +"\t更改之後的值爲:"+integer.get());
}
}
你能猜到答案麼?
是否自選並替換 true 更改之後的值爲:10
是否自選並替換 false 更改之後的值爲:10
第一次修改,期望值爲5,主內存也爲5,修改成功,爲10。
第二次修改,期望值爲5,主內存爲10,修改失敗。
CAS原理
在翻了源碼之後,大致可以總結出兩個關鍵點:
- 自旋;
- unsafe類。
當點開compareAndSet
方法後:
// AtomicInteger類內部
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
通過這個方法,我們可以找出AtomicInteger
內部維護了volatile int value
和private static final Unsafe unsafe
兩個比較重要的參數。(注意value是用volatile修飾)
變量valueOffset,表示該變量在內存中的偏移地址,因爲Unsafe就是根據內存偏移地址獲取數據的。
變量value用volatile修飾,保證了多線程之間的內存可見性。
// AtomicInteger類內部
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
然後我們通過compareAndSwapInt
找到了unsafe類核心方法:
//unsafe內部類
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
AtomicInteger.
compareAndSwapInt()
調用了Unsafe.
compareAndSwapInt()
方法。Unsafe
類的大部分方法都是native
的,用來像C語言一樣從底層操作內存。
這個方法的var1和var2,就是根據對象和偏移量得到在主內存的快照值var5。然後compareAndSwapInt
方法通過var1和var2得到當前主內存的實際值。如果這個實際值跟快照值相等,那麼就更新主內存的值爲var5+var4。如果不等,那麼就一直循環,一直獲取快照,一直對比,直到實際值和快照值相等爲止。
比如有A、B兩個線程
- 一開始都從主內存中拷貝了原值爲3;
- A線程執行到
var5=this.getIntVolatile
,即var5=3。此時A線程掛起; - B修改原值爲4,B線程執行完畢,由於加了volatile,所以這個修改是立即可見的;
- A線程被喚醒,執行
this.compareAndSwapInt()
方法,發現這個時候主內存的值不等於快照值3,所以繼續循環,重新從主內存獲取。 - 線程A重新獲取value值,因爲變量value被volatile修飾,所以其他線程對它的修改,線程A總是能夠看到,線程A繼續執行compareAndSwapInt進行比較替換,直至成功。
ABA問題
所謂ABA問題,其實用最通俗易懂的話語來總結就是狸貓換太子
就是比較並交換的循環,存在一個時間差,而這個時間差可能帶來意想不到的問題。
比如有兩個線程A、B:
- 一開始都從主內存中拷貝了原值爲3;
- A線程執行到
var5=this.getIntVolatile
,即var5=3。此時A線程掛起; - B修改原值爲4,B線程執行完畢;
- 然後B覺得修改錯了,然後再重新把值修改爲3;
- A線程被喚醒,執行
this.compareAndSwapInt()
方法,發現這個時候主內存的值等於快照值3,(但是卻不知道B曾經修改過),修改成功。
儘管線程A CAS操作成功,但不代表就沒有問題。有的需求,比如CAS,只注重頭和尾,只要首尾一致就接受。但是有的需求,還看重過程,中間不能發生任何修改,這就引出了AtomicReference
原子引用。
AtomicReference原子引用
AtomicInteger
對整數進行原子操作,如果是一個POJO呢?可以用AtomicReference
來包裝這個POJO,使其操作原子化。
User user1 = new User("Jack",25);
User user2 = new User("Lucy",21);
AtomicReference<User> atomicReference = new AtomicReference<>();
atomicReference.set(user1);
System.out.println(atomicReference.compareAndSet(user1,user2)); // true
System.out.println(atomicReference.compareAndSet(user1,user2)); //false
本質是比較的是兩個對象的地址是否相等。
使用場景:
一個線程使用student對象,另一個線程負責定時讀表,更新這個對象。那麼就可以用AtomicReference
AtomicStampedReference和ABA問題的解決
使用AtomicStampedReference
類可以解決ABA問題。這個類維護了一個“版本號”Stamp,其實有點類似樂觀鎖的意思。
在進行CAS操作的時候,不僅要比較當前值,還要比較版本號。只有兩者都相等,才執行更新操作。
AtomicStampedReference.compareAndSet(expectedReference,newReference,oldStamp,newStamp);
CAS總結
任何技術都不是完美的,當然,CAS也有他的缺點:
CAS實際上是一種自旋鎖,
- 一直循環,開銷比較大。
- 只能保證一個變量的原子操作,多個變量依然要加鎖。
- 引出了ABA問題(AtomicStampedReference可解決)。
而他的使用場景適合在一些併發量不高、線程競爭較少的情況,加鎖太重。但是一旦線程衝突嚴重的情況下,循環時間太長,爲給CPU帶來很大的開銷。