Java並發--Java中的CAS操作和實現原理


版權聲明:本文為博主原創文章,遵循 CC 4.0 by-sa 版權協議,轉載請附上原文出處鏈接和本聲明。
本文鏈接: https://blog.csdn.net/CringKong/article/details/80533917

這幾天准備梳理一下Java多線程和並發的相關知識,主要是系統的梳理一下J.U.C包里的一些東西,特別是以前看過很多遍的AQS和實現類,還有各種並發安全的集合類。最重要的就是這個CAS操作,可以說是整個J.U.C包的靈魂之處。

 

 

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標記
  }
}

2019.8.15補充:好的~我又回來了,不得不說寫這篇文章當時屬實淺嘗輒止,現在需要解釋一下這段匯編代碼

  int mp = os::is_MP();

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變量。
  1. A線程寫volatile變量,隨后B線程用CAS更新這個volatile變量。
  1. A線程用CAS更新一個volatile變量,隨后B線程用CAS更新這個volatile變量。
  1. A線程用CAS更新一個volatile變量,隨后B線程讀這個volatile變量。

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

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

如果我們仔細分析concurrent包的源代碼實現,會發現一個通用化的實現模式:

  1. 首先,聲明共享變量為volatile;
  2. 然后,使用CAS的原子條件更新來實現線程之間的同步;
  3. 同時,配合以volatile的讀/寫和CAS所具有的volatile讀和寫的內存語義來實現線程之間的通信。

這里寫圖片描述

4.小結

其實本來還有更多的基礎要講一講,但是這一篇博客不能太長了,關於JVM內存結構,JMM模型,還有volatile關鍵字和Java中原生的的同步鎖.,這些以后希望能補全,當然也是我自己再次學習的過程記錄下來。


參考資料:

  1. JAVA CAS原理深度分析——超多干貨
  2. Java並發編程:volatile關鍵字解析


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM