JVM 源碼分析:深入理解 CAS

前言

什麼是 CAS

Java 中的 CAS

JVM 中的 CAS

在上一篇文章中,我們完成了源碼的編譯和調試環境的搭建。

鑑於 CAS 的實現原理比較簡單, 然而很多人對它不夠了解,所以本篇將從 CAS 入手,首先介紹它的使用,然後分析它在 Hotsport 虛擬機中的具體實現。

什麼是 CAS

CAS(Compare And Swap,比較並交換)通常指的是這樣一種原子操作:針對一個變量,首先比較它的內存值與某個期望值是否相同,如果相同,就給它賦一個新值。

CAS 的邏輯用僞代碼描述如下:

if(value== expectedValue) {value= newValue;}

以上僞代碼描述了一個由比較和賦值兩階段組成的複合操作,CAS 可以看作是它們合併後的整體——一個不可分割的原子操作,並且其原子性是直接在硬件層面得到保障的,後面我會具體介紹。

Java 中的 CAS

在 Java 中,CAS 操作是由 Unsafe 類提供支持的,該類定義了三種針對不同類型變量的 CAS 操作,如圖。

它們都是 native 方法,由 Java 虛擬機提供具體實現,這意味着不同的 Java 虛擬機對它們的實現可能會略有不同。

下面我將通過代碼演示一下它們的功能,以 compareAndSwapInt 爲例。

首先需要得到 Unsafe 對象。由於 Unsafe 被設計爲單例類,並且它的獲取實例的方法只允許被基礎類庫中的類調用,因此,我們自己的類要想獲取 Unsafe 對象,只能通過反射實現。

獲取 Unsafe 對象的代碼如下:

privatestatic Unsafe getUnsafe() {try{        Field theUnsafeField = Unsafe.class.getDeclaredField("theUnsafe");        theUnsafeField.setAccessible(true);return(Unsafe) theUnsafeField.get(Unsafe.class);}catch(NoSuchFieldException | IllegalAccessException e) {thrownew Error(e);    }}

Unsafe 的 compareAndSwapInt 方法接收 4 個參數,分別是:對象實例、字段偏移量、字段期望值、字段新值。該方法會針對指定對象實例中的相應偏移量的字段執行 CAS 操作。

獲取字段偏移量的代碼如下:

privatestaticlonggetFieldOffset(Unsafeunsafe, Class clazz, String fieldName){try{returnunsafe.objectFieldOffset(clazz.getDeclaredField(fieldName));    }catch(NoSuchFieldException e) {thrownewError(e);    }}

演示代碼如下:

publicstaticvoid main(String[] args) {    Unsafeunsafe= getUnsafe();    long offset = getFieldOffset(unsafe, Entity.class,"x");    boolean successful;    successful =unsafe.compareAndSwapInt(entity, offset,0,3);    System.out.println(successful +"\t"+ entity.x);    successful =unsafe.compareAndSwapInt(entity, offset,3,5);    System.out.println(successful +"\t"+ entity.x);    successful =unsafe.compareAndSwapInt(entity, offset,3,8);    System.out.println(successful +"\t"+ entity.x);}

在我們的演示代碼中,我們首先得到 Unsafe 對象,然後得到 Entity 中的 x 字段的偏移量(Entity 是我們自定義的實體類)。接下來是針對 entity.x 的 3 次 CAS 操作,分別試圖將它從 0 改成 3、從 3 改成 5、從 3 改成 8。

執行結果如下:

可以看到,由於 entity.x 的原始值爲 0 ,所以第一次 CAS 成功地將它更新爲 3 ,第二次 CAS 也成功地將它更新爲 5 ,但是在第三次 CAS 時,由於 entity.x 的當前值 5 與期望值 3 不相同,所以 CAS 失敗, entity.x 並沒有得到更新,它的值仍然是 5 。

以上就是 CAS 在 Java 中的直觀體現,它是所有併發原子類型的基礎。下面我們來看一下它的底層實現。

JVM 中的 CAS

關於上面演示的 compareAndSwapInt 方法,Hotspot 虛擬機對它的實現如下:

爲了更加直觀,我在這裏打上了斷點,並聯合上面的 Java 代碼一起調試。上圖顯示了當前線程停在了斷點處的對 Atomic::cmpxchg 方法的調用上。

Atomic::cmpxchg 方法非常關鍵,它是 Hotspot 虛擬機對 CAS 操作的封裝。我們將斷點跟進方法內部,從 “Variables” 標籤頁中可以觀察到,當前 Java 虛擬機正在處理上述 Java 程序的第一次 CAS 請求,準備將 entity.x 的值從 0 改成 3,如圖。

Atomic::cmpxchg 方法的定義如上圖所示,它首先通過 os::is_MP() 判斷當前執行環境是否爲多處理器環境,然後嵌入一段彙編代碼,這段彙編代碼會執行一條 cmpxchgl 指令,同時把 exchange_value 等變量作爲操作數,當它執行完成之後,方法將直接返回 exchange_value 的值。

從中可以看出, cmpxchgl 彙編指令是整個 Atomic::cmpxchg 方法的核心。

順便補充一下,彙編代碼中的 LOCK_IF_MP 是一個宏,這個宏的作用是,在多處理器環境下,爲 cmpxchgl 指令添加 lock 前綴,以達到內存屏障的效果。內存屏障能夠在目標指令執行之前,保障多個處理器之間的緩存一致性,由於單處理器環境下並不需要內存屏障,故做此判斷。

cmpxchgl 指令是包含在 x86 架構及 IA-64 架構中的一個原子條件指令,在我們的例子中,它會首先比較 dest 指針指向的內存值是否和 compare_value 的值相等,如果相等,則雙向交換 dest 與 exchange_value ,否則就單方面地將 dest 指向的內存值交給 “exchange_value 。這條指令完成了整個 CAS 操作,因此它也被稱爲 CAS 指令。

事實上,現代指令集架構基本上都會提供 CAS 指令,例如 x86 和 IA-64 架構中的 cmpxchgl 指令和 comxchgq 指令,sparc 架構中的 cas 指令和 casx 指令等等。

不管是 Hotspot 中的 Atomic::cmpxchg 方法,還是 Java 中的 compareAndSwapInt 方法,它們本質上都是對相應平臺的 CAS 指令的一層簡單封裝。CAS 指令作爲一種硬件原語,有着天然的原子性,這也正是 CAS 的價值所在。

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