1. 簡介
CAS
,Compare And Swap
,即比較並交換。整個AQS
同步組件、Atomic
原子類操作等等都是以CAS
實現的,甚至ConcurrentHashMap
在1.8
的版本中也調整爲了CAS
+Synchronized
。可以說CAS
是整個JUC
的基石。
2. CAS 分析
在CAS
中有三個參數:內存值V、舊的預期值A
、要更新的值B
,當且僅當內存值V
的值等於舊的預期值A
時纔會將內存值V
的值修改爲B
,否則什麼都不幹。其僞代碼如下:
if(this.value == A){
this.value = B
return true;
}else{
return false;
}
JUC
下的atomic
類都是通過CAS
來實現的,下面就以AtomicInteger
爲例來闡述CAS
的實現。如下:
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;
Unsafe
是CAS
的核心類,Java
無法直接訪問底層操作系統,而是通過本地(native
)方法來訪問。不過儘管如此,JVM
還是開了一個後門:Unsafe
,它提供了硬件級別的原子操作。valueOffset
爲變量值在內存中的偏移地址,unsafe
就是通過偏移地址來得到數據的原值的。value
當前值,使用volatile
修飾,保證多線程環境下看見的是同一個。
我們就以AtomicInteger的addAndGet()
方法來做說明,先看源代碼:
public final int addAndGet(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
}
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;
}
內部調用unsafe
的getAndAddInt
方法,在getAndAddInt
方法中主要是看compareAndSwapInt
方法:
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, intvar5);
該方法爲本地方法,有四個參數,分別代表:對象、對象的地址、預期值、修改值。
CAS
可以保證一次的讀-改-寫操作是原子操作,在單處理器上該操作容易實現,但是在多處理器上實現就有點兒複雜了。
CPU
提供了兩種方法來實現多處理器的原子操作:總線加鎖或者緩存加鎖。
1、總線加鎖:總線加鎖就是就是使用處理器提供的一個LOCK#
信號,當一個處理器在總線上輸出此信號時,其他處理器的請求將被阻塞住,那麼該處理器可以獨佔使用共享內存。但是這種處理方式顯得有點兒霸道,不厚道,他把CPU
和內存之間的通信鎖住了,在鎖定期間,其他處理器都不能其他內存地址的數據,其開銷有點兒大。所以就有了緩存加鎖。
2、緩存加鎖:其實針對於上面那種情況我們只需要保證在同一時刻對某個內存地址的操作是原子性的即可。緩存加鎖就是緩存在內存區域的數據如果在加鎖期間,當它執行鎖操作寫回內存時,處理器不在輸出LOCK#
信號,而是修改內部的內存地址,利用緩存一致性協議來保證原子性。緩存一致性機制可以保證同一個內存區域的數據僅能被一個處理器修改,也就是說當CPU1
修改緩存行中的i
時使用緩存鎖定,那麼CPU2
就不能同時緩存了i
的緩存行。
3. CAS 缺陷
CAS
雖然高效地解決了原子操作,但是還是存在一些缺陷的,主要表現在三個方法:循環時間太長、只能保證一個共享變量原子操作、ABA問題。
循環時間太長:如果CAS
一直不成功呢?這種情況絕對有可能發生,如果自旋CAS
長時間的不成功,則會給CPU
帶來非常大的開銷。在JUC
中有些地方就限制了CAS
自旋的次數,例如BlockingQueue的SynchronousQueue
。
只能保證一個共享變量原子操作:看了CAS
的實現就知道這隻能針對一個共享變量,如果是多個共享變量就只能使用鎖了,當然如果你有辦法把多個變量整成一個變量,利用CAS
也不錯。例如讀寫鎖中state
的高低位
ABA問題 :CAS
需要檢查操作值有沒有發生改變,如果沒有發生改變則更新。但是存在這樣一種情況:如果一個值原來是A
,變成了B
,然後又變成了A
,那麼在CAS
檢查的時候會發現沒有改變,但是實質上它已經發生了改變,這就是所謂的ABA
問題。對於ABA
問題其解決方案是加上版本號,即在每個變量都加上一個版本號,每次改變時加1
,即A —> B —> A,變成1A —> 2B —> 3A。
CAS
的ABA
隱患問題,解決方案則是版本號,Java提供了AtomicStampedReference
來解決。AtomicStampedReference
通過包裝[E,Integer]
的元組來對對象標記版本戳stamp
,從而避免ABA
問題。
AtomicStampedReference
的compareAndSet()
方法定義如下:
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
Pair<V> current = pair;
return
expectedReference == current.reference &&
expectedStamp == current.stamp &&
((newReference == current.reference &&
newStamp == current.stamp) ||
casPair(current, Pair.of(newReference, newStamp)));
}
compareAndSet
有四個參數,分別表示:預期引用、更新後的引用、預期標誌、更新後的標誌。源碼部分很好理解預期的引用 當前引用,預期的標識 當前標識,如果更新後的引用和標誌和當前的引用和標誌相等則直接返回true
,否則通過Pai
r生成一個新的pair
對象與當前pair CAS
替換。
Pair
爲AtomicStampedReference
的內部類,主要用於記錄引用和版本戳信息(標識),定義如下:
private static class Pair<T> {
final T reference;
final int stamp;
private Pair(T reference, int stamp) {
this.reference = reference;
this.stamp = stamp;
}
static <T> Pair<T> of(T reference, int stamp) {
return new Pair<T>(reference, stamp);
}
}
private volatile Pair<V> pair;
Pair
記錄着對象的引用和版本戳,版本戳爲int
型,保持自增。同時Pair
是一個不可變對象,其所有屬性全部定義爲final
,對外提供一個of
方法,該方法返回一個新建的Pari
對象。pair
對象定義爲volatile
,保證多線程環境下的可見性。在AtomicStampedReference
中,大多方法都是通過調用Pair
的of
方法來產生一個新的Pair
對象,然後賦值給變量pair
。如set
方法:
public void set(V newReference, int newStamp) {
Pair<V> current = pair;
if (newReference != current.reference || newStamp != current.stamp)
this.pair = Pair.of(newReference, newStamp);
}