Java 多線程學習(3) CAS 底層原理學習之我是如何從 Java 源碼看到 openjdk 源碼再到彙編碼、intel 手冊的

原本是準備閱讀 j.u.c 包下 ConcurrentHashMap 的底層源碼,理解 ConcurrentHashMap 的實現原理的,看了一點點發現裏面用到了不少 CAS。而且 atomic 和 locks 這兩個包中也大量使用了 CAS,因此就先把 CAS 的原理搞清楚了以後再繼續後面的內容。html

看了一大堆文章,也是把它弄懂了。令我沒想到的是,本身居然從 Java 源碼看到 openjdk 源碼及彙編碼,最後還看了一些 intel 手冊的內容,最終不只學會了 CAS,還學到了許多其餘的知識。java

慢慢發現,其實深刻研究某一個知識點的實現,仍是蠻有意思的,只是過程可能有點艱辛。linux


CAS 是樂觀鎖的一種實現方式,是一種輕量級鎖,j.u.c 中不少工具類的實現就是基於 CAS 的。c++

一、什麼是 CAS?

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 的底層實現原理

CAS 的核心是 Unsafe 類。而當你去看 Unsafe 的源碼的時候,發現裏面調用的是 native 方法。而要看 native 方法的實現,確實須要花很大一番功夫,而且基本上都是 C++ 代碼。緩存

在通過一番折騰後,至少我大體知道了 Unsafe 類中的 native 方法的調用鏈及關鍵的 C++ 源碼。數據結構

compareAndSwapInt 爲例,這個本地方法在 openjdk 中依次調用的 C++ 代碼爲:多線程

(1)unsafe.cppopenjdk/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

  • 若是是運行在 windows_x86(windows 系統,x86 處理器)下

atomic_windows_x86.inline.hppopenjdk/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
  }
}
複製代碼

點擊查看源碼

  • 若是是運行在 linux_x86 (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__ 表示彙編的開始

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 原子操做指令修改該數據,完成對數據的修改。

三、一些思考和疑問

(1)既然 CAS 具備 volatile 的讀和寫的內存語義,那爲何還須要把變量聲明成 volatile 呢?

volatile 的讀和寫的內存語義實際上是經過 lock 指令前綴實現的,如圖:

volatile putfield

而 CAS 在系統是多核處理器時也會添加 lock 指令前綴,這兩個不就是重複了嗎?

原本想經過工具查看 CAS 這一部分的彙編碼的,不過 Java 代碼的彙編碼不包含這一部分,也就不知道這一部分的彙編碼究竟是啥樣子的,由於這部分是由 C++ 實現的。

這個做以下猜想:

  • 對於不走 CAS,單純走 set 方法的,volatile 能夠保證這些賦值操做的內存可見性;
  • 對於單核處理器來講,CAS 是沒有加 lock 指令的,這種狀況僅靠 cmpxchg 可否保證各個線程的本地緩存失效呢?對 volatile 變量作 CAS 是否能夠避免這個問題?

以上僅是我我的的一些理解,不必定正確,歡迎你們來一塊兒討論。

(2)總線鎖定和多線程中獲取鎖是一個道理,只不過它的粒度要更小;CPU Cache 也能夠類比到線程的本地工做內存。

四、CAS 的優勢

不加鎖,在併發衝突程度不高的狀況下,效率極高。(能夠參考樂觀鎖的優勢)

五、CAS 存在哪些缺陷?

(1)CAS + 自旋 ==> 可能致使:循環時間長,CPU 開銷大

大多數狀況下,CAS 是配合自旋來實現對單個共享變量的更新的。

若是自旋 CAS 長時間不成功(說明併發衝突大),會給 CPU 帶來很是大的執行開銷。

(2)ABA 問題

首先明白一點:CAS 自己是一個原子操做,不存在 ABA 問題。

不過使用 CAS 更新數據通常須要三個步驟:

  • 取數
  • 處理數據
  • CAS 更新數據

在這個過程當中可能出現 ABA 問題。上面三個步驟不是一個原子操做,因此可能出現下面這種狀況:

  • 線程 thread1 查詢 A 的值爲 a,開始處理數據
  • 線程 thread2 執行完三個步驟將 A 的值更新爲 b
  • 線程 thread3 執行完三個步驟將 A 的值從 b 又更新回 a
  • 線程 thread1 處理完數據,獲得 c ,這時它對比內存中的值 a,將 A 的值更新爲 c

線程 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 中會出現嗎?我以爲仍是能夠思考思考的。

(3)不支持多個共享變量的原子操做

從上面的介紹來看,CAS 自己就是針對單個共享變量的,對於多個共享變量,固然是不支持的。

固然,若是把多個共享變量合併成一個共享變量(放在一個對象裏面),也是能夠進行 CAS 操做。

這就看怎麼理解多個共享變量了,若是說一個共享變量的多個屬性能夠被稱之爲多個共享變量,那麼 CAS 也是能夠支持的。

六、結語

學 CAS ,最後學到的知識有:

  • 可以粗淺閱讀一些 openjdk 的 C++ 源碼

  • 加深對 volatile 的理解

  • lock 指令的做用

  • 內存屏障

  • 如何反彙編 Java 字節碼

  • 以及一些工具的使用

收穫頗豐!給本身點個贊!哈哈哈~

參考資料:

(1)深刻淺出CAS

(2)JAVA CAS原理深度分析

(3)Why Memory Barriers?中文翻譯(上)

(4)intel IA32 手冊 8.一、8.二、8.3節www.intel.com/content/dam…)

相關文章
相關標籤/搜索