在Java并發(fā)中户侥,我們最初接觸的應(yīng)該就是synchronized關(guān)鍵字了,但是synchronized屬于重量級鎖峦嗤,很多時候會引起性能問題蕊唐,volatile也是個不錯的選擇,但是volatile不能保證原子性烁设,只能在某些場合下使用替梨。
像synchronized這種獨占鎖屬于悲觀鎖,它是在假設(shè)一定會發(fā)生沖突的装黑,那么加鎖恰好有用副瀑,除此之外,還有樂觀鎖恋谭,樂觀鎖的含義就是假設(shè)沒有發(fā)生沖突糠睡,那么我正好可以進行某項操作,如果要是發(fā)生沖突呢箕别,那我就重試直到成功铜幽,樂觀鎖最常見的就是CAS。
我們在讀Concurrent包下的類的源碼時串稀,發(fā)現(xiàn)無論是ReenterLock內(nèi)部的AQS除抛,還是各種Atomic開頭的原子類,內(nèi)部都應(yīng)用到了CAS母截,最常見的就是我們在并發(fā)編程時遇到的i++這種情況到忽。傳統(tǒng)的方法肯定是在方法上加上synchronized關(guān)鍵字:
public class Test {
public volatile int i;
public synchronized void add() {
i++;
}
}
但是這種方法在性能上可能會差一點,我們還可以使用AtomicInteger清寇,就可以保證i原子的++了喘漏。
public class Test {
public AtomicInteger i;
public void add() {
i.getAndIncrement();
}
}
我們來看getAndIncrement的內(nèi)部:
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;
}
這里我們見到compareAndSwapInt這個函數(shù),它也是CAS縮寫的由來华烟。那么仔細(xì)分析下這個函數(shù)做了什么呢翩迈?
首先我們發(fā)現(xiàn)compareAndSwapInt前面的this,那么它屬于哪個類呢盔夜,我們看上一步getAndAddInt负饲,前面是unsafe堤魁。這里我們進入的Unsafe類。這里要對Unsafe類做個說明返十。結(jié)合AtomicInteger的定義來說:
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
// setup to use Unsafe.compareAndSwapInt for updates
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;
...
在AtomicInteger數(shù)據(jù)定義的部分妥泉,我們可以看到,其實實際存儲的值是放在value中的洞坑,除此之外我們還獲取了unsafe實例盲链,并且定義了valueOffset。再看到static塊迟杂,懂類加載過程的都知道刽沾,static塊的加載發(fā)生于類加載的時候,是最先初始化的逢慌,這時候我們調(diào)用unsafe的objectFieldOffset從Atomic類文件中獲取value的偏移量悠轩,那么valueOffset其實就是記錄value的偏移量的。
再回到上面一個函數(shù)getAndAddInt攻泼,我們看var5獲取的是什么火架,通過調(diào)用unsafe的getIntVolatile(var1, var2),這是個native方法忙菠,具體實現(xiàn)到JDK源碼里去看了何鸡,其實就是獲取var1中,var2偏移量處的值牛欢。var1就是AtomicInteger骡男,var2就是我們前面提到的valueOffset,這樣我們就從內(nèi)存里獲取到現(xiàn)在valueOffset處的值了。
現(xiàn)在重點來了傍睹,compareAndSwapInt(var1, var2, var5, var5 + var4)其實換成compareAndSwapInt(obj, offset, expect, update)比較清楚隔盛,意思就是如果obj內(nèi)的value和expect相等,就證明沒有其他線程改變過這個變量拾稳,那么就更新它為update吮炕,如果這一步的CAS沒有成功,那就采用自旋的方式繼續(xù)進行CAS操作访得,取出乍一看這也是兩個步驟了啊龙亲,其實在JNI里是借助于一個CPU指令完成的。所以還是原子操作悍抑。
CAS底層原理
CAS底層使用JNI調(diào)用C代碼實現(xiàn)的鳄炉,如果你有Hotspot源碼,那么在Unsafe.cpp里可以找到它的實現(xiàn):
static JNINativeMethod methods_15[] = {
//省略一堆代碼...
{CC"compareAndSwapInt", CC"("OBJ"J""I""I"")Z", FN_PTR(Unsafe_CompareAndSwapInt)},
{CC"compareAndSwapLong", CC"("OBJ"J""J""J"")Z", FN_PTR(Unsafe_CompareAndSwapLong)},
//省略一堆代碼...
};
我們可以看到compareAndSwapInt實現(xiàn)是在Unsafe_CompareAndSwapInt里面搜骡,再深入到Unsafe_CompareAndSwapInt:
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
UnsafeWrapper("Unsafe_CompareAndSwapInt");
oop p = JNIHandles::resolve(obj);
jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END
p是取出的對象拂盯,addr是p中offset處的地址,最后調(diào)用了Atomic::cmpxchg(x, addr, e), 其中參數(shù)x是即將更新的值记靡,參數(shù)e是原內(nèi)存的值磕仅。代碼中能看到cmpxchg有基于各個平臺的實現(xiàn)珊豹,這里選擇Linux X86平臺下的源碼分析:
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;
}
這是一段小匯編,asm說明是ASM匯編榕订,volatile禁止編譯器優(yōu)化
// Adding a lock prefix to an instruction on MP machine
#define LOCK_IF_MP(mp) "cmp $0, " #mp "; je 1f; lock; 1: "
os::is_MP判斷當(dāng)前系統(tǒng)是否為多核系統(tǒng),如果是就給總線加鎖蜕便,所以同一芯片上的其他處理器就暫時不能通過總線訪問內(nèi)存劫恒,保證了該指令在多處理器環(huán)境下的原子性。
在正式解讀這段匯編前轿腺,我們來了解下嵌入?yún)R編的基本格式:
asm ( assembler template
: output operands /* optional */
: input operands /* optional */
: list of clobbered registers /* optional */
);
- template就是cmpxchgl %1,(%3)表示匯編模板
- output operands表示輸出操作數(shù),=a對應(yīng)eax寄存器
- input operand 表示輸入?yún)?shù)两嘴,%1 就是exchange_value, %3是dest, %4就是mp, r表示任意寄存器族壳,a還是eax寄存器
- list of clobbered registers就是些額外參數(shù)憔辫,cc表示編譯器cmpxchgl的執(zhí)行將影響到標(biāo)志寄存器, memory告訴編譯器要重新從內(nèi)存中讀取變量的最新值,這點實現(xiàn)了volatile的感覺仿荆。
那么表達式其實就是cmpxchgl exchange_value ,dest贰您,我們會發(fā)現(xiàn)%2也就是compare_value沒有用上,這里就要分析cmpxchgl的語義了拢操。cmpxchgl末尾l表示操作數(shù)長度為4锦亦,上面已經(jīng)知道了。cmpxchgl會默認(rèn)比較eax寄存器的值即compare_value和exchange_value的值令境,如果相等杠园,就把dest的值賦值給exchange_value,否則,將exchange_value賦值給eax
最終舔庶,JDK通過CPU的cmpxchgl指令的支持抛蚁,實現(xiàn)AtomicInteger的CAS操作的原子性。
CAS 的問題
ABA問題
CAS需要在操作值的時候檢查下值有沒有發(fā)生變化惕橙,如果沒有發(fā)生變化則更新瞧甩,但是如果一個值原來是A,變成了B吕漂,又變成了A亲配,那么使用CAS進行檢查時會發(fā)現(xiàn)它的值沒有發(fā)生變化,但是實際上卻變化了惶凝。這就是CAS的ABA問題吼虎。
常見的解決思路是使用版本號。在變量前面追加上版本號苍鲜,每次變量更新的時候把版本號加一思灰,那么A-B-A 就會變成1A-2B-3A。
目前在JDK的atomic包里提供了一個類AtomicStampedReference來解決ABA問題混滔。這個類的compareAndSet方法作用是首先檢查當(dāng)前引用是否等于預(yù)期引用洒疚,并且當(dāng)前標(biāo)志是否等于預(yù)期標(biāo)志歹颓,如果全部相等,則以原子方式將該引用和該標(biāo)志的值設(shè)置為給定的更新值油湖。
循環(huán)時間長開銷大
上面我們說過如果CAS不成功巍扛,則會原地自旋,如果長時間自旋會給CPU帶來非常大的執(zhí)行開銷乏德。
為什么cas更快撤奸?
其實可以看出cas本質(zhì)也是加鎖了的,但為啥往往cas比用lock更快喊括,其實lock本質(zhì)也是使用了cas的胧瓜,但當(dāng)競爭比較激烈時,它會有線程的阻塞郑什,掛起等府喳,cas顯然不會有。它更輕量級蘑拯。