synchronized
關鍵字了,但是synchronized
屬於重量級鎖,很多時候會引起效能問題,volatile
也是個不錯的選擇,但是volatile
不能保證原子性,只能在某些場合下使用。
像synchronized
這種獨佔鎖屬於悲觀鎖,它是在假設一定會發生衝突的,那麼加鎖恰好有用,除此之外,還有樂觀鎖,樂觀鎖的含義就是假設沒有發生衝突,那麼我正好可以進行某項操作,如果要是發生衝突呢,那我就重試直到成功,樂觀鎖最常見的就是CAS
。
我們在讀Concurrent包下的類的原始碼時,發現無論是ReenterLock內部的AQS,還是各種Atomic開頭的原子類,內部都應用到了CAS
,最常見的就是我們在併發程式設計時遇到的i++
這種情況。傳統的方法肯定是在方法上加上synchronized
關鍵字:
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
的內部:
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
複製程式碼
再深入到getAndAddInt
():
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
的定義來說:
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底層原理
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 的問題
- ABA問題
CAS需要在操作值的時候檢查下值有沒有發生變化,如果沒有發生變化則更新,但是如果一個值原來是A,變成了B,又變成了A,那麼使用CAS進行檢查時會發現它的值沒有發生變化,但是實際上卻變化了。這就是CAS的ABA問題。
常見的解決思路是使用版本號。在變數前面追加上版本號,每次變數更新的時候把版本號加一,那麼A-B-A
就會變成1A-2B-3A
。
目前在JDK的atomic包裡提供了一個類AtomicStampedReference
來解決ABA問題。這個類的compareAndSet方法作用是首先檢查當前引用是否等於預期引用,並且當前標誌是否等於預期標誌,如果全部相等,則以原子方式將該引用和該標誌的值設定為給定的更新值。
- 迴圈時間長開銷大
上面我們說過如果CAS不成功,則會原地自旋,如果長時間自旋會給CPU帶來非常大的執行開銷。