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 對象的代碼如下:

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, 03);
    System.out.println(successful + "\t" + entity.x);

    successful = unsafe.compareAndSwapInt(entity, offset, 35);
    System.out.println(successful + "\t" + entity.x);

    successful = unsafe.compareAndSwapInt(entity, offset, 38);
    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 的值相等,如果相等,則雙向交換 destexchange_value,否則就單方面地將 dest 指向的內存值交給 ``exchange_value。這條指令完成了整個 CAS 操作,因此它也被稱為 CAS 指令。

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

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


免責聲明!

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



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