前言
什麼是 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 的價值所在。