Java中的CAS操作和實現原理

1.什麼是CAS?

CAS:Compare and Swap, 翻譯成比較並交換。

看到這個定義,可以說是沒有任何意義的一句話,但是確實最能概括CAS操作過程的一句話。

CAS 操作包含三個操作數 —— 內存位置(V)、預期原值(A)和新值(B)。 如果內存位置的值與預期原值相匹配,那麼處理器會自動將該位置值更新爲新值 。否則,處理器不做任何操作。無論哪種情況,它都會在 CAS 指令之前返回該 位置的值。(在 CAS 的一些特殊情況下將僅返回 CAS 是否成功,而不提取當前 值。)CAS 有效地說明了“我認爲位置 V 應該包含值 A;如果包含該值,則將 B 放到這個位置;否則,不要更改該位置,只告訴我這個位置現在的值即可。”

以下這段JAVA代碼,基本上反映了CAS操作的過程。但是請注意,真實的CAS操作是由CPU完成的,CPU會確保這個操作的原子性,CAS遠非JAVA代碼能實現的功能(下面我們會看到CAS的彙編代碼)。

	/**
	* 假設這段代碼是原子性的,那麼CAS其實就是這樣一個過程
	*/
	public boolean compareAndSwap(int v,int a,int b) {
		if (v == a) {
			v = b;
			return true;
		}else {
			return false;
		}
	}

通常將 CAS 用於同步的方式是從地址 V 讀取值 A,執行多步計算來獲得新 值 B,然後使用 CAS 將 V 的值從 A 改爲 B。如果 V 處的值尚未同時更改,則 CAS 操作成功。

這段話的意思是,CAS操作可以防止內存中共享變量出現髒讀髒寫問題,多核的CPU在多線程的情況下經常出現的問題,通常我們採用鎖來避免這個問題,但是CAS操作避免了多線程的競爭鎖,上下文切換和進程調度。

類似於 CAS 的指令允許算法執行讀-修改-寫操作,而無需害怕其他線程同時 修改變量,因爲如果其他線程修改變量,那麼 CAS 會檢測它(並失敗),算法 可以對該操作重新計算。

2.JAVA中的CAS操作實現原理

CAS通過調用JNI的代碼實現的。JNI:Java Native Interface爲JAVA本地調用,允許java調用其他語言。

以Unsafe類的compareAndSwapInt()方法爲例來說,compareAndSwapInt就是藉助C語言和彙編代碼來實現的。
下面從分析比較常用的CPU(intel x86)來解釋CAS的實現原理。
下面是JDK中sun.misc.Unsafe類的compareAndSwapInt()方法的源代碼:

// native方法,是沒有其Java代碼實現的,而是需要依靠JDK和JVM的實現
public final native boolean compareAndSwapInt(Object o, long offset,
                                              int expected,
                                              int x);

可以看到這是個本地方法。這個本地方法在openjdk中依次調用的c++代碼爲:unsafe.cpp,atomic.cpp和atomicwindowsx86.inline.hpp。這個本地方法的最終實現在openjdk的如下位置:openjdk-7-fcs-src-b147-27jun2011\openjdk\hotspot\src\oscpu\windowsx86\vm\ atomicwindowsx86.inline.hpp(對應於windows操作系統,X86指令集)。下面是對應於intel x86處理器的源代碼的片段:

// Adding a lock prefix to an instruction on MP machine
// VC++ doesn't like the lock prefix to be on a single line
// so we can't insert a label after the lock prefix.
// By emitting a lock prefix, we can define a label after it.
#define LOCK_IF_MP(mp) __asm cmp mp, 0  \
                       __asm je L0      \
                       __asm _emit 0xF0 \
                       __asm L0:

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 // 如果是多核處理器就會在這行指令前加Lock標記
  }
}

os::is_MP() 會返回當前JVM運行所在機器是否爲多核CPU,當然返回1代表true,0代表false

然後是一段內嵌彙編,C/C++支持內嵌彙編,大家知道這個特性就好,我來通俗易懂的解釋一下這段彙編的大體意思。

  __asm {
    mov edx, dest             # 取Atomic::cmpxchg方法的參數dest內存地址存入寄存器edx
    mov ecx, exchange_value   # 取Atomic::cmpxchg方法的參數exchange_value內存地址存入寄存器ecx
    mov eax, compare_value    # 取Atomic::cmpxchg方法的參數compare_value內存地存入寄存器eax
    LOCK_IF_MP(mp)            # 如果是多核處理器,就在下一行彙編代碼前加上lock前綴
    cmpxchg dword ptr [edx], ecx # 比較ecx和eax的中內存地址的中存的變量值,如果相等就寫入edx內存地址中,否則不
  }

x86彙編指令cmpxchg本身保證了原子性,其實就是cpu的CAS操作的實現,那麼問題來了,爲什麼保證了原子性還需要在多核處理器中加上lock前綴呢?

答案是:多核處理器中不能保證可見性,lock前綴可以保證這行彙編中所有值的可見性,這個問題的原因是多核CPU中緩存導致的(x86中罪魁禍首是store buffer的存在)。
這樣通過lock前綴保障多核處理器的可見性,然後通過cmpxchg指令完成CPU上原子性的CAS操作,完美解決問題!
多說一句,這只是x86中的實現方式,對於其他平臺,還是有不同的方式實現,這點希望讀者一定要搞清楚。
這段彙編代碼看不懂也沒關係,但其大意是使用CPU的鎖機制,確保了整個CAS操作的原子性。關於CPU中的鎖機制和CPU的原子操作 ——CPU中的原子操作。

3.concurrent包中CAS的應用

由於java的CAS同時具有 volatile 讀和volatile寫的內存語義,因此Java線程之間的通信現在有了下面四種方式:
1、A線程寫volatile變量,隨後B線程讀這個volatile變量。
2、A線程寫volatile變量,隨後B線程用CAS更新這個volatile變量。
3、A線程用CAS更新一個volatile變量,隨後B線程用CAS更新這個volatile變量。
4、A線程用CAS更新一個volatile變量,隨後B線程讀這個volatile變量。

注:volatile 關鍵字保證了變量的可見性,根據JAVA內存模型,每一個線程都有自己的棧內存,不同線程的棧內存裏的變量有可能因爲棧內的操作而不同,而 CPU又是直接操作棧中的數據並保存在自己的緩存中,所以多核CPU就出現了很大的問題,而volatile修飾的變量,保證了CPU各個核心不會從棧內存中和 緩存中讀數據,而是直接從堆內存中讀數據,而且寫操作會直接寫回堆內存中,從而保證了多線程間共享變量的可見性和局部順序性(但不保證原子性),關於volatile——Java併發編程:volatile關鍵字解析

Java的CAS操作可以實現現代CPU上硬件級別的原子指令(不是依靠JVM或者操作系統的鎖機制),而同時volatile關鍵字又保證了線程間共享變量的可見性和指令的順序性,因此憑藉這兩種手段,就可以實現不依靠操作系統實現的鎖機制來保證併發時共享變量的一致性。

如果我們仔細分析concurrent包的源代碼實現,會發現一個通用化的實現模式:
首先,聲明共享變量爲volatile;
然後,使用CAS的原子條件更新來實現線程之間的同步;
同時,配合以volatile的讀/寫和CAS所具有的volatile讀和寫的內存語義來實現線程之間的通信。

在這裏插入圖片描述

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