這是一個傷感的微故事
當年忽然消失,拿走我2w元的前女友又忽然回來找我。我心情是複雜的,本以為她要還錢,沒想到是複合。我們長談到深夜,主要是她在聊自己的經歷,我聽完, 好言相勸她還錢,她甩給我2w,我假裝生氣,奪門而出。現已物是人非,2w塊錢還是很香,像撿來的一樣。
出來抽菸的我似乎想通了CAS的ABA問題。
什麼是CAS
CAS(Compare And Swap),比較替換。它有3個核心參數:
- V:要修改的變量
- A :V的原有值,即線程讀取V時的值
- B:要修改成的值
在對V做修改之前,先檢查V的值是否與預期值A相等,如果相等就把V值更新為B,否則不更新,CAS失敗。
下面是一段偽代碼幫助大家理解
//把a的值從3改成4,這裡V就是a,A是3,B是4
int a = 3;
cas(a,3,4);
先用3與a此刻的值(a所指向的內存值)比較,如果相等,則a會被修改成4。否則修改失敗。失敗後如何處理?
它常與自旋(通常用死循環實現)配合使用,修改失敗則不停重試CAS,直到成功。整個過程無需鎖定a,是一種樂觀鎖的實現。檢測
衝突和數據更新本身也是樂觀鎖的一貫作風。
解決線程同步
解決線程同步的方式一般有2種
- 悲觀鎖
- 樂觀鎖
悲觀鎖的缺陷很明顯,比如死鎖、加鎖釋放鎖帶來的性能消耗。
用樂觀鎖在大部分情況下可以提高性能,上面說的CAS就是一種樂觀鎖的實現。但CAS只能解決多線程的原子性,並不能解決另外2個問
題,順序性和一致性。而volatile正好能解決這2個問題,你說巧不巧。所以CAS和volatile必定能走到一起。
當然java大師們早已發現了其中的奧秘。用CAS+volatile作為基石開發出了大名鼎鼎的J.U.C包。
其中最簡單直觀的要數atomic包。
多線程場景下,給某個數做自增的時候通常是加synchronized關鍵字
int count = 0;
synchronized void c() {
for (int i = 0; i < 10000; i++)
count++;
}
JUC包中的AtomicInteger提供的incrementAndGet 完全可以代替synchronized和count++的功能。
AtomicInteger count = new AtomicInteger(0);
void c() {
for (int i = 0; i < 10000; i++)
count.incrementAndGet();
}
AtomicInteger:
//value被volatile修飾
private volatile int value;
public AtomicInteger(int initialValue) {
value = initialValue;
}
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
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:
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
count.incrementAndGet最終調用的方法是Unsafe類的compareAndSwapInt方法,這個方法被native修飾。也就意味著它已經調用了
虛擬機C/C++實現的代碼。筆者試著跟了一下Hotspot的代碼,具體過程就不演示了。最後代碼如下:
inline jint Atomic::cmpxchg (jint exchange_value, volatile jint*dest, jint compare_value) {
int mp = os::is_MP();
__asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)"
: "=a" (exchange_value)
: "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
: "cc", "memory");
return exchange_value;
}
關鍵看LOCK_IF_MP:如果是多處理器要加lock,保證CAS的原子性,最終執行的是彙編指令:cmpxchg(compare and exchange)。
lock cmpxchg
彙編是可以直接操作CPU的,cmpxchg指令也是cpu支持的原語。但cmpxchg並不是原子的,所以在多處理器下還需要加lock來保證CAS的原子性。
有同學說了,這還不是加鎖了。是的,只是在系統級別加的鎖,要比在jvm上加鎖效率高的多。
我們說的CAS無鎖並不是絕對的無鎖,而是在java層面表現為無鎖。
看不懂c++、彙編指令不要緊,但是lock cmpxchg 一定要記住,因為面試要問。
CAS的ABA問題
ABA :數據從A變成B,又變回A,假裝什麼都沒發生。
上面的例子中,如果a從3變為5,又變回3,那麼CAS在對比時a的值依然是3,替換成功。
這好像也沒什麼問題,反正目的就是把a的值改成4。就像前女友還我的2萬塊錢,雖然RMB已經改版,新版2w依然可以接受。
但不是所有人都能接受失而復得。
一般基礎類型的ABA問題大家都能接受。但是複雜類型就不一定了,比如下面的Zoo類
public class Zoo {
private Tiger tiger = new Tiger("母老虎");
public static void main(String[] args) {
Zoo zoo = new Zoo();
zoo.setTiger(new Tiger("公老虎"));
}
}
Zoo裡面有成員變量tiger。當new一個Zoo實例zoo的時候,zoo的內存值已經確定了,不會因為其成員變量的改變而改變。
這樣的ABA問題就比較嚴重。就像我的前任一樣。
解決ABA問題通用的辦法就是加版本號,A->B->A改成A1-B2->A3。JUC用帶版本號的 AtomicStampedReference 來解決ABA問題。
//initialRef:要修改的引用V,initialStamp:版本號,可以是時間戳或任何整數
public AtomicStampedReference(V initialRef, int initialStamp) {
pair = Pair.of(initialRef, initialStamp);
}
本文著重是理論分析,而且AtomicStampedReference比較簡單,就不做展開討論。
ABA終於想通了 ,多麼痛的領悟,你的前任回來找你了嗎。記得注意是否存在ABA問題哦。
(本故事純屬虛構,如有雷同,請不要對號入座)。