java的cas原理(jvm底層實現的彙編語言)

簡介

CAS (Compare And Swap)比較並交換操作。

CAS 有 3 個操作數,分別是內存位置 V、舊的預期值 A 和擬修改的新值 B。當且僅當 V 符合預期值 A 時,用新值 B 更新 V 的值,否則什麼都不做。

jvm實現原理

以java中compareAndSwapInt方法爲例子,位於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

Atomic::cmpxchg方法是在atomic_linux_x86.inline.hpp:openjdk/hotspot/src/os_cpu/windows_x86/vm/atomic_linux_x86.inline.hpp

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;
}

該方法是會直接用匯編語句來執行,只需要關注LOCK_IF_MP(%4) "cmpxchgl %1,(%3)",後面的全是給這句話賦值用的。

#define LOCK_IF_MP(mp) "cmp $0, " #mp "; je 1f; lock; 1: "

// 翻譯過來就是
// lock cmpxchgl
cmpxchgl
Opcode Instruction Description
0F B0/r CMPXCHG r/m8,r8 Compare AL with r/m8. If equal, ZF is set and r8 is loaded into r/m8. Else, clear ZF and load r/m8 into AL.
0F B1/r CMPXCHG r/m16,r16 Compare AX with r/m16. If equal, ZF is set and r16 is loaded into r/m16. Else, clear ZF and load r/m16 into AL.
0F B1/r CMPXCHG r/m32,r32 Compare EAX with r/m32. If equal, ZF is set and r32 is loaded into r/m32. Else, clear ZF and load r/m32 into AL

Compares the value in the AL, AX, or EAX register (depending on the size of the operand) with the first operand (destination operand). If the two values are equal, the second operand (source operand) is loaded into the destination operand. Otherwise, the destination operand is loaded into the AL, AX, or EAX register.

This instruction can be used with a LOCK prefix to allow the instruction to be executed atomically. To simplify the interface to the processor’s bus, the destination operand receives a write cycle without regard to the result of the comparison. The destination operand is written back if the comparison fails; otherwise, the source operand is written into the destination. (The processor never produces a locked read without also producing a locked write.)

翻譯:

比較AL、AX或EAX寄存器中的值(取決於操作數的大小)和第一個操作數(目標操作數)。如果兩個值相等,則將第二個操作數(源操作數)加載到目標操作數中。否則,目標操作數被加載到AL、AX或EAX寄存器中。

這個指令可以與一個鎖前綴一起使用,以允許指令自動執行。爲了簡化與處理器總線的接口,目標操作數接收一個寫週期,而不考慮比較的結果。如果比較失敗,則回寫目標操作數;否則,源操作數被寫入目標。(處理器在不產生鎖寫的情況下永遠不會產生鎖讀。)

作用:就是原子級別的cas操作。

lock

LOCK指令前綴會設置處理器的LOCK#信號(譯註:這個信號會使總線鎖定,阻止其他處理器接管總線訪問內存),直到使用LOCK前綴的指令執行結束,這會使這條指令的執行變爲原子操作。在多處理器環境下,設置LOCK#信號能保證某個處理器對共享內存的獨佔使用。

LOCK指令前綴只能用於以下指令,並且要求指令目的操作數爲內存操作數,如果源操作數爲內存操作數,就會產生undefined opcode異常:ADD, ADC, AND, BTC, BTR, BTS, CMPXCHG, CMPXCH8B,CMPXCHG16B, DEC, INC, NEG, NOT, OR, SBB, SUB, XOR, XADD, XCHG。LOCK指令前綴用於其他任何指令時,也會產生如果源操作數爲內存操作數,就會產生undefined opcode異常。另外,XCHG指令默認會設置LOCK#信號,無論是否使用LOCK指令前綴。

LOCK指令前綴經常用於BTS指令,用來在共享內存進行一次read-modify-write操作。

從P6處理器家族開始,如果使用了LOCK指令前綴的指令要訪問的目的地址的內容已經緩存在了cache中,那麼LOCK#信號一般就不會被設置,但是當前處理器的cache會被鎖定,然後緩存一致性(cache coherency )機制會自動確保操作的原子性。

有興趣可以看完整的:

在修改內存操作時,使用LOCK前綴去調用加鎖的讀-修改-寫操作,這種機制用於多處理器系統中處理器之間進行可靠的通訊,具體描述如下:

  1. 在Pentium和早期的IA-32處理器中,LOCK前綴會使處理器執行當前指令時產生一個LOCK#信號,這種總是引起顯式總線鎖定出現。
  2. 在Pentium4、Inter Xeon和P6系列處理器中,加鎖操作是由高速緩存鎖或總線鎖來處理。如果內存訪問有高速緩存且隻影響一個單獨的高速緩存行,那麼操作中就會調用高速緩存鎖,而系統總線和系統內存中的實際區域內不會被鎖定。同時,這條總線上的其它Pentium4、Intel Xeon或者P6系列處理器就回寫所有已修改的數據並使它們的高速緩存失效,以保證系統內存的一致性。如果內存訪問沒有高速緩存且/或它跨越了高速緩存行的邊界,那麼這個處理器就會產生LOCK#信號,並在鎖定操作期間不會響應總線控制請求。

從Pentium 4,Intel Xeon及P6處理器開始,intel在原有總線鎖的基礎上做了一個很有意義的優化:如果要訪問的內存區域(area of memory)在lock前綴指令執行期間已經在處理器內部的緩存中被鎖定(即包含該內存區域的緩存行當前處於獨佔或以修改狀態),並且該內存區域被完全包含在單個緩存行(cache line)中,那麼處理器將直接執行該指令。由於在指令執行期間該緩存行會一直被鎖定,其它處理器無法讀/寫該指令要訪問的內存區域,因此能保證指令執行的原子性。這個操作過程叫做緩存鎖定(cache locking),緩存鎖定將大大降低lock前綴指令的執行開銷,但是當多處理器之間的競爭程度很高或者指令訪問的內存地址未對齊時,仍然會鎖住總線。從Pentium 4,Intel Xeon及P6處理器開始,包括以後的處理器都有了緩存鎖。

32位IA-32處理器支持對系統內存中的某個區域進行加鎖的原子操作。這些操作常用來管理共享的數據結構(如信號量、段描述符、系統段或頁表),兩個或多個處理器可能同時會修改這些數據結構中的同一數據域或標誌。處理器使用三個相互依賴的機制來實現加鎖的原子操作:

  1. 保證原子操作。
  2. 總線加鎖,使用LOCK#信號和LOCK指令前綴。
  3. 高速緩存相干性協議,確保對高速緩存中的數據結構執行原子操作(高速緩存鎖),這種機制存在於Pentium4、Intel Xeon和P6系列處理器中。

IA-32處理器提供有一個LOCK#信號,會在某些關鍵內存操作期間被自動激活,去鎖定系統總線。當這個輸出信號發出的時候,來自其他處理器或總線代理的控制請求將被阻塞。軟件能夠通過預先在指令前添加LOCK前綴來指定需要LOCK語義的其它場合。

在Intel386、Intel486、Pentium處理器中,明確地對指令加鎖會導致LOCK#信號的產生。由硬件設計人員來保證系統硬件中LOCK#信號的可用性,以控制處理器間的內存訪問。
對於Pentinum4、Intel Xeon以及P6系列處理器,如果被訪問的內存區域是在處理器內部進行高速緩存的,那麼通常不發出LOCK#信號;相反,加鎖只應用於處理器的高速緩存。

爲顯式地強制執行LOCK語義,軟件可以在下列指令修改內存區域時使用LOCK前綴。當LOCK前綴被置於其它指令之前或者指令沒有對內存進行寫操作(也就是說目標操作數在寄存器中)時,會產生一個非法操作碼異常(#UD)。

  1. 位測試和修改指令(BTS、BTR、BTC)
  2. 交換指令(XADD、CMPXCHG、CMPXCHG8B)
  3. 自動假設有LOCK前綴的XCHG指令
  4. 下列單操作數的算數和邏輯指令:INC、DEC、NOT、NEG
  5. 下列雙操作數的算數和邏輯指令:ADD、ADC、SUB、SBB、AND、OR、XOR

一個加鎖的指令會保證對目標操作數所在的內存區域加鎖,但是系統可能會將鎖定區域解釋得稍大一些。

軟件應該使用相同的地址和操作數長度來訪問信號量(用作處理器之間發送信號的共享內存)。例如,如果一個處理器使用一個字來訪問信號量,其它處理器就不應該使用一個字節來訪問這個信號量。

總線鎖的完整性不受內存區域對齊的影響。加鎖語義會一直持續,以滿足更新整個操作數所需的總線週期個數。但是,建議加鎖訪問應該對齊在它們的自然邊界上,以提升系統性能:

  1. 任何8位訪問的邊界(加鎖或不加鎖)。
  2. 鎖定的字訪問的16位邊界。
  3. 鎖定的雙字訪問的32位邊界。
  4. 鎖定的四字訪問的64位邊界。

對所有其它的內存操作和所有可見的外部事件來說,加鎖的操作都是原子的。所有取指令和頁表操作能夠越過加鎖的指令。加鎖的指令可用於同步一個處理器寫數據而另一個處理器讀數據的操作。

IA-32架構提供了幾種機制用來強化或弱化內存排序模型,以處理特殊的編程情形。這些機制包括:

  1. I/O指令、加鎖指令、LOCK前綴以及串行化指令等,強制在處理器上進行較強的排序。
  2. SFENCE指令(在Pentium III中引入)和LFENCE指令、MFENCE指令(在Pentium4和Intel Xeon處理器中引入)提供了某些特殊類型內存操作的排序和串行化功能。

題外話

volatile 的讀和寫的內存語義其實是通過 lock 指令前綴實現的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章