原本是準備閱讀 j.u.c 包下 ConcurrentHashMap 的底層源碼,理解 ConcurrentHashMap 的實現原理的,看了一點點發現裏面用到了不少 CAS。而且 atomic 和 locks 這兩個包中也大量使用了 CAS,因此就先把 CAS 的原理搞清楚了以後再繼續後面的內容。html
看了一大堆文章,也是把它弄懂了。令我沒想到的是,本身居然從 Java 源碼看到 openjdk 源碼及彙編碼,最後還看了一些 intel 手冊的內容,最終不只學會了 CAS,還學到了許多其餘的知識。java
慢慢發現,其實深刻研究某一個知識點的實現,仍是蠻有意思的,只是過程可能有點艱辛。linux
CAS 是樂觀鎖的一種實現方式,是一種輕量級鎖,j.u.c 中不少工具類的實現就是基於 CAS 的。c++
CAS (Compare And Swap)比較並交換操做。git
CAS 有 3 個操做數,分別是內存位置 V、舊的預期值 A 和擬修改的新值 B。當且僅當 V 符合預期值 A 時,用新值 B 更新 V 的值,不然什麼都不作。github
用一段僞代碼來幫助理解 CAS:windows
Object A = getValueFromV();// 先讀取內存位置 V 處的值 A
Object B = A + balaba;// 對 A 作必定處理,獲得新值 B
// 下面這部分就是 CAS,經過硬件指令實現
if( A == actualValueAtV ) {// actualValueAtV 爲執行當前原子操做時內存位置 V 處的值
setNewValueToV(B);// 將新值 B 更新到內存位置 V 處
} else {
doNothing();// 說明有其餘線程改過內存位置 V 處的值了,A 已經不是最新值了,因此基於 A 處理獲得的新值 B 是不對的
}
複製代碼
CAS 的核心是 Unsafe 類。而當你去看 Unsafe 的源碼的時候,發現裏面調用的是 native 方法。而要看 native 方法的實現,確實須要花很大一番功夫,而且基本上都是 C++ 代碼。緩存
在通過一番折騰後,至少我大體知道了 Unsafe 類中的 native 方法的調用鏈及關鍵的 C++ 源碼。數據結構
以 compareAndSwapInt
爲例,這個本地方法在 openjdk 中依次調用的 C++ 代碼爲:多線程
(1)unsafe.cpp
:openjdk/hotspot/src/share/vm/prims/unsafe.cpp
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
複製代碼
(2)atomic_****_****.inline.hpp
atomic_windows_x86.inline.hpp
:openjdk/hotspot/src/os_cpu/windows_x86/vm/atomic_windows_x86.inline.hpp
inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
// alternative for InterlockedCompareExchange
int mp = os::is_MP();
__asm {
mov edx, dest
mov ecx, exchange_value
mov eax, compare_value
LOCK_IF_MP(mp)
cmpxchg dword ptr [edx], ecx
}
}
複製代碼
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__
表示彙編的開始
volatile
表示禁止編譯器優化
LOCK_IF_MP
是個內聯函數
#define LOCK_IF_MP(mp) "cmp $0, " #mp "; je 1f; lock; 1: "
複製代碼
以上幾個參考自佔小狼的深刻淺出 CAS。
os::is_MP()
這個函數是判斷當前系統是不是多核處理器。
因此這個地方應該就是生成彙編碼,我就只關注了這一行 LOCK_IF_MP(%4) "cmpxchgl %1,(%3)"
,畢竟後面的也看不懂。。。
這裏至關因而若是當前系統是多核處理器則會在添加 lock 指令前綴,不然就不加。
關於
lock
指令前綴說明:
- 在《深刻理解 Java 虛擬機》中,做者解釋 volatile 的內存可見性原理時提到(371 頁):「關鍵在於 lock 前綴,查詢 IA32 手冊,它的做用是使得本 CPU 的 Cache 寫入了內存,該寫入動做也會引發別的 CPU 或者別的內核無效化( Invalidate )其 Cache,這種操做至關於對 Cache 中的變量作了一次前面介紹 Java 內存模式中所說的 ' store 和 write ' 操做。」
- intel IA32 手冊 中 8.1 LOCKED ATOMIC OPERATIONS 關於 lock 前綴的含義:
- 保證原子操做
- 總線鎖定,經過使用 LOCK# 信號和 LOCK 指令前綴
- 高速緩存一致性協議,確保能夠對高速緩存的數據結構執行原子操做(緩存鎖定)
- lock 指令前綴也具備禁止指令重排序做用:能夠經過閱讀 intel IA32 手冊中 8.2.2 Memory Ordering in P6 and More Recent Processor Families 和 8.2.3 Examples Illustrating the Memory-Ordering Principles 兩節的內容得出。
看到這裏,CAS 的底層實現原理也就很顯然了,實際上就是:lock cmpxchg
其中,cmpxchg
是硬件級別的原子操做指令,lock
前綴保證這個指令執行結果的內存可見性和禁止指令的重排序。
關於
lock cmpxchg
的一些我的理解:因爲lock
指令前綴會鎖定總線(或者是緩存鎖定),因此在該 CPU 執行時總線是處於獨佔狀態,該 CPU 經過總線廣播一條read invalidate
信息,經過高速緩存一致性協議(MESI),將其他 CPU 中該數據的 Cache 置爲invalid
狀態(若是存在該數據的 Cache ),從而得到了對該數據的獨佔權,以後再執行cmpxchg
原子操做指令修改該數據,完成對數據的修改。
volatile 的讀和寫的內存語義實際上是經過 lock 指令前綴實現的,如圖:
而 CAS 在系統是多核處理器時也會添加 lock 指令前綴,這兩個不就是重複了嗎?
原本想經過工具查看 CAS 這一部分的彙編碼的,不過 Java 代碼的彙編碼不包含這一部分,也就不知道這一部分的彙編碼究竟是啥樣子的,由於這部分是由 C++ 實現的。
這個做以下猜想:
lock
指令的,這種狀況僅靠 cmpxchg
可否保證各個線程的本地緩存失效呢?對 volatile 變量作 CAS 是否能夠避免這個問題?以上僅是我我的的一些理解,不必定正確,歡迎你們來一塊兒討論。
不加鎖,在併發衝突程度不高的狀況下,效率極高。(能夠參考樂觀鎖的優勢)
大多數狀況下,CAS 是配合自旋來實現對單個共享變量的更新的。
若是自旋 CAS 長時間不成功(說明併發衝突大),會給 CPU 帶來很是大的執行開銷。
首先明白一點:CAS 自己是一個原子操做,不存在 ABA 問題。
不過使用 CAS 更新數據通常須要三個步驟:
在這個過程當中可能出現 ABA 問題。上面三個步驟不是一個原子操做,因此可能出現下面這種狀況:
線程 thread1 在處理數據的過程當中,實際上 A 的值已經經歷了 a -> b -> a
的過程,可是對於線程 thread1 來講判斷不出來,因此線程 thread1 仍是能夠將 A 的值更新爲 c。這就是咱們說的 ABA 問題。
這裏我用 Java 代碼模擬了一下 ABA 問題,有興趣的能夠去看一下:CasABAProblem
ABA 問題可能帶來的問題是什麼呢?換句話說,a -> b -> a
這個過程可能會有哪些反作用?
思考了好久,沒想到什麼好的例子。。。等想到了以後再來更新。。。下面咱們來看如何避免 ABA 問題。
其實避免 ABA 問題其實很簡單,只須要給數據添加一個版本號。上面例子中的 a -> b -> a
的過程就會變成 1a -> 2b -> 3a
,當線程 thread1 處理完數據,發現 1a != 3a
,因此也就不會更新 A 的值了。能夠參考 j.u.c atomic 包下 AtomicStampedReference 類,它就是添加了一個 stamp 字段做爲數據的版本號。
我還試了一下 compareAndSwapObject 方法,發現這個方法比較的是對象的引用,由於無論我怎麼修改對象中的屬性,compareAndSwapObject 都能執行成功。。。因此 Unsafe 中 compareAndSwap 的 compare 是否就能夠用 == 來等價呢?看了一下,AtomicReference 中 compareAndSet(V expect, V update) 上的文檔好像也確實是這麼寫的:
* Atomically sets the value to the given updated value * if the current value {@code ==} the expected value. 複製代碼
我看到不少人的博客上面寫了 ABA 問題,舉了鏈表或棧的出棧入棧相關的例子。參考 wikipedia 上面的例子:比較並交換
裏面是用 C 操做的堆,對堆進行了一系列的 pop 和 push 操做。並解釋說:因爲內存管理機制中普遍使用的內存重用機制,致使 NodeC 的地址與以前的 NodeA 一致。
這種狀況在 Java 中會出現嗎?我以爲仍是能夠思考思考的。
從上面的介紹來看,CAS 自己就是針對單個共享變量的,對於多個共享變量,固然是不支持的。
固然,若是把多個共享變量合併成一個共享變量(放在一個對象裏面),也是能夠進行 CAS 操做。
這就看怎麼理解多個共享變量了,若是說一個共享變量的多個屬性能夠被稱之爲多個共享變量,那麼 CAS 也是能夠支持的。
學 CAS ,最後學到的知識有:
可以粗淺閱讀一些 openjdk 的 C++ 源碼
加深對 volatile 的理解
lock 指令的做用
內存屏障
如何反彙編 Java 字節碼
以及一些工具的使用
收穫頗豐!給本身點個贊!哈哈哈~
參考資料:
(1)深刻淺出CAS