前言
在上一篇文章中,我們完成了原始碼的編譯和除錯環境的搭建。
鑑於 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 物件的程式碼如下:
private static Unsafe getUnsafe() {
try {
Field theUnsafeField = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafeField.setAccessible(true);
return (Unsafe) theUnsafeField.get(Unsafe.class);
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new Error(e);
}
}
Unsafe 的 compareAndSwapInt
方法接收 4 個引數,分別是:物件例項、欄位偏移量、欄位期望值、欄位新值。該方法會針對指定物件例項中的相應偏移量的欄位執行 CAS 操作。
獲取欄位偏移量的程式碼如下:
private static long getFieldOffset(Unsafe unsafe, Class clazz, String fieldName) {
try {
return unsafe.objectFieldOffset(clazz.getDeclaredField(fieldName));
} catch (NoSuchFieldException e) {
throw new Error(e);
}
}
演示程式碼如下:
public static void main(String[] args) {
Unsafe unsafe = 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 的價值所在。