synchronized
關鍵字了,可是
synchronized
屬於重量級鎖,不少時候會引發性能問題,
volatile
也是個不錯的選擇,可是
volatile
不能保證原子性,只能在某些場合下使用。
像synchronized
這種獨佔鎖屬於悲觀鎖,它是在假設必定會發生衝突的,那麼加鎖剛好有用,除此以外,還有樂觀鎖,樂觀鎖的含義就是假設沒有發生衝突,那麼我正好能夠進行某項操做,若是要是發生衝突呢,那我就重試直到成功,樂觀鎖最多見的就是CAS
。java
咱們在讀Concurrent包下的類的源碼時,發現不管是ReenterLock內部的AQS,仍是各類Atomic開頭的原子類,內部都應用到了CAS
,最多見的就是咱們在併發編程時遇到的i++
這種狀況。傳統的方法確定是在方法上加上synchronized
關鍵字:編程
public class Test {
public volatile int i;
public synchronized void add() {
i++;
}
}
複製代碼
可是這種方法在性能上可能會差一點,咱們還能夠使用AtomicInteger
,就能夠保證i
原子的++
了。bash
public class Test {
public AtomicInteger i;
public void add() {
i.getAndIncrement();
}
}
複製代碼
咱們來看getAndIncrement
的內部:併發
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
複製代碼
再深刻到getAndAddInt
():app
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
這個函數,它也是CAS
縮寫的由來。那麼仔細分析下這個函數作了什麼呢?函數
首先咱們發現compareAndSwapInt
前面的this
,那麼它屬於哪一個類呢,咱們看上一步getAndAddInt
,前面是unsafe
。這裏咱們進入的Unsafe
類。這裏要對Unsafe
類作個說明。結合AtomicInteger
的定義來講:oop
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
數據定義的部分,咱們能夠看到,其實實際存儲的值是放在value
中的,除此以外咱們還獲取了unsafe
實例,而且定義了valueOffset
。再看到static
塊,懂類加載過程的都知道,static
塊的加載發生於類加載的時候,是最早初始化的,這時候咱們調用unsafe
的objectFieldOffset
從Atomic
類文件中獲取value
的偏移量,那麼valueOffset
其實就是記錄value
的偏移量的。源碼分析
再回到上面一個函數getAndAddInt
,咱們看var5
獲取的是什麼,經過調用unsafe
的getIntVolatile(var1, var2)
,這是個native方法,具體實現到JDK源碼裏去看了,其實就是獲取var1
中,var2
偏移量處的值。var1
就是AtomicInteger
,var2
就是咱們前面提到的valueOffset
,這樣咱們就從內存裏獲取到如今valueOffset
處的值了。性能
如今重點來了,compareAndSwapInt(var1, var2, var5, var5 + var4)
其實換成compareAndSwapInt(obj, offset, expect, update)
比較清楚,意思就是若是obj
內的value
和expect
相等,就證實沒有其餘線程改變過這個變量,那麼就更新它爲update
,若是這一步的CAS
沒有成功,那就採用自旋的方式繼續進行CAS
操做,取出乍一看這也是兩個步驟了啊,其實在JNI
裏是藉助於一個CPU
指令完成的。因此仍是原子操做。優化
CAS底層使用JNI
調用C代碼實現的,若是你有Hotspot
源碼,那麼在Unsafe.cpp
裏能夠找到它的實現:
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實現是在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處的地址,最後調用了Atomic::cmpxchg(x, addr, e)
, 其中參數x是即將更新的值,參數e是原內存的值。代碼中能看到cmpxchg有基於各個平臺的實現,這裏我選擇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__
禁止編譯器優化
// 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
判斷當前系統是否爲多核系統,若是是就給總線加鎖,因此同一芯片上的其餘處理器就暫時不能經過總線訪問內存,保證了該指令在多處理器環境下的原子性。
在正式解讀這段彙編前,咱們來了解下嵌入彙編的基本格式:
asm ( assembler template
: output operands /* optional */
: input operands /* optional */
: list of clobbered registers /* optional */
);
複製代碼
template就是cmpxchgl %1,(%3)
表示彙編模板
output operands表示輸出操做數,=a
對應eax寄存器
input operand 表示輸入參數,%1
就是exchange_value
, %3
是dest
, %4
就是mp
, r
表示任意寄存器,a
仍是eax
寄存器
list of clobbered registers就是些額外參數,cc
表示編譯器cmpxchgl
的執行將影響到標誌寄存器, memory
告訴編譯器要從新從內存中讀取變量的最新值,這點實現了volatile
的感受。
那麼表達式其實就是cmpxchgl exchange_value ,dest
,咱們會發現%2
也就是compare_value
沒有用上,這裏就要分析cmpxchgl
的語義了。cmpxchgl
末尾l
表示操做數長度爲4
,上面已經知道了。cmpxchgl
會默認比較eax
寄存器的值即compare_value
和exchange_value
的值,若是相等,就把dest
的值賦值給exchange_value
,不然,將exchange_value
賦值給eax
。具體彙編指令能夠查看Intel手冊CMPXCHG
最終,JDK經過CPU的cmpxchgl
指令的支持,實現AtomicInteger
的CAS
操做的原子性。
CAS須要在操做值的時候檢查下值有沒有發生變化,若是沒有發生變化則更新,可是若是一個值原來是A,變成了B,又變成了A,那麼使用CAS進行檢查時會發現它的值沒有發生變化,可是實際上卻變化了。這就是CAS的ABA問題。 常見的解決思路是使用版本號。在變量前面追加上版本號,每次變量更新的時候把版本號加一,那麼A-B-A
就會變成1A-2B-3A
。 目前在JDK的atomic包裏提供了一個類AtomicStampedReference
來解決ABA問題。這個類的compareAndSet方法做用是首先檢查當前引用是否等於預期引用,而且當前標誌是否等於預期標誌,若是所有相等,則以原子方式將該引用和該標誌的值設置爲給定的更新值。
上面咱們說過若是CAS不成功,則會原地自旋,若是長時間自旋會給CPU帶來很是大的執行開銷。