天天看點

從彙編底層全面解析 CAS 的來龍去脈

作者:愛做夢的程式員

一、引言

對于 Java 開發者而言,關于 CAS ,我們一般當做黑盒來進行使用,不需要去打開這個黑盒。

但随着目前程式員行業的發展,我們有必要打開這個黑盒,去探索其中的奧妙。

本期 CAS 源碼解析文章,将帶你領略 CAS 源碼的奧秘

本源碼文章吸收了之前 Spring、Kakfa、JUC源碼文章的教訓,将不再一行一行的帶大家分析源碼,我們将一些不重要的部分當做黑盒處理,以便我們更快、更有效的閱讀源碼。

雖然現在是網際網路寒冬,但乾坤未定,你我皆是黑馬!

廢話不多說,發車!

二、使用

在 Java 中,CAS 操作是通過 JDK 提供的 java.util.concurrent.atomic 包下的 Atomic 系列類來實作的。

例如,AtomicInteger 類提供了原子性的加法、減法、比較和設定等操作,它們都是通過 CAS 操作來實作的。

Java 中的 CAS 操作通常使用 sun.misc.Unsafe 類來實作,因為 CAS 操作需要直接操作記憶體,而 Unsafe 類提供了直接操作記憶體的方法。

雖然 Unsafe 類是 Java 平台的内部實作細節,但是在一些高性能的并發程式設計庫和架構中,仍然會使用 Unsafe 類來實作 CAS 操作。

java複制代碼public class Test {
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger();
        for (int i = 0; i < 10; i++) {
            atomicInteger.getAndIncrement();
        }
        System.out.println(atomicInteger.get());
    }
}
           

三、原理

CAS(Compare and Swap)是一種并發程式設計中常用的原子操作,用于實作多線程環境下的同步和互斥。

CAS 操作包括三個參數:

  • 記憶體位址 V
  • 原始值 A
  • 新值 B。

如果目前記憶體位址的值等于 原始值 A,則将記憶體位址的值修改為 新值 B,否則不進行任何操作。

CAS 操作是原子的,即在同一時刻隻有一個線程能夠成功執行該操作。

從彙編底層全面解析 CAS 的來龍去脈

如上所示:

  • 第一步:CPU 擷取記憶體位址上的資料 V
  • 第二步:CPU 将 原始值 與 資料 V 做對比
  • 第三步: 如果相等,将 記憶體位址 的 資料V 更換成 新值 如果不相等,則不進行操作

四、源碼

上面是 CAS 一些的基本使用和原理,老粉都知道,小黃主打的就是一個 源碼硬核

我們繼續分析其 HotSpot 中的實作

在 Java 代碼中,我們追到下面這行代碼就沒辦法繼續往下追了

java複制代碼public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
           

我們翻開 HotSpot 源碼:

c++複制代碼Atomic::cmpxchg_ptr(lock, obj()->mark_addr(), mark)
           

在不同的作業系統下面,實作不同。

1、Linux作業系統源碼

以 linux x86 為例,它的 int 類型的 CAS 實作如下:

  • 第一個參數是 exchange_value(新值)
  • 第二個參數是 dest(目标位址)
  • 第三個參數是 compare_value(原值)
c++複制代碼inline void* Atomic::cmpxchg_ptr(void* exchange_value, volatile void* dest, void* compare_value) {
  return (void*)cmpxchg((jint)exchange_value, (volatile jint*)dest, (jint)compare_value);
}
           

咱們繼續往下追:

c++複制代碼inline jint Atomic::cmpxchg(jint exchange_value, volatile jint* dest, jint compare_value) {
  int mp = os::is_MP();
  __asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)"
                    : "=a" (exchange_value)
                    : "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
                    : "cc", "memory");
  return exchange_value;
}
           

在 Linux 環境下,最終調的就是這個方法

2、Window作業系統源碼

但實際上來說,Linux 下的方法不太友善我們去閱讀源碼,我們來看看 Window 下的實作

c++複制代碼// atomic_windows_x86.inline.hpp
#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
  }
}

           

我們一行一行的去進行分析:

  • mov edx, dest:擷取記憶體位址 dest 資料放至 edx 寄存器中
  • mov ecx, exchange_value:将 新值 放入到 ecx 寄存器中
  • mov eax, compare_value:将 原值放入到 eax 寄存器中
  • LOCK_IF_MP(mp):根據目前是否是多核進行加鎖

當然,前面都不是我們的重點,我們的重點是下面這一行代碼:

c++複制代碼cmpxchg dword ptr [edx], ecx
           

首先我們先來看 dword ptr [edx] 這個是啥意思

dword :全稱是 doubleword

ptr:全稱是 pointer,與前面的 dword 連起來使用,表明通路的記憶體單元是一個雙字單元

[edx]:表示一個記憶體單元,edx 是寄存器,dest 指針值存放在 edx 中。那麼 [edx] 表示記憶體位址為 dest 的記憶體單元

是以,dword ptr [edx] 的意思:通路記憶體位址為 dest 的雙字記憶體單元

有人可能會疑惑,這裡也沒有我們上面說的 eax 裡面的寄存器資料呀

不要着急,奧秘就在 cmpxchg 這個裡面

我們看一下官方對于 cmpxchg 指令的定義:

java複制代碼Compares the value in the AL, AX, EAX, or RAX register with the first operand (destination operand). If the two values are equal, the second operand (source operand) is loaded into the destination operand. Otherwise, the destination operand is loaded into the AL, AX, EAX or RAX register. RAX register is available only in 64-bit mode.

This instruction can be used with a LOCK prefix to allow the instruction to be executed atomically. To simplify the interface to the processor’s bus, the destination operand receives a write cycle without regard to the result of the comparison. The destination operand is written back if the comparison fails; otherwise, the source operand is written into the destination. (The processor never produces a locked read without also producing a locked write.)
    
    
// 翻譯
将 AL、AX、EAX 或 RAX 寄存器中的值與第一個操作數(目标操作數)進行比較。如果兩個值相等,則将第二個操作數(源操作數)加載到目标操作數中。否則,目标操作數被加載到 AL、AX、EAX 或 RAX 寄存器中。RAX 寄存器僅在 64 位模式下可用。

該指令可以與 LOCK 字首一起使用,以允許指令以原子方式執行。為了簡化與處理器總線的接口,目标操作數接收一個寫周期而不考慮比較的結果。如果比較失敗則寫回目标操作數;否則,源操作數被寫入目标。(處理器永遠不會在不産生鎖定寫入的情況下産生鎖定讀取。)
           

是以,我們在這裡看到了 EAX 寄存器的出現,将 AL、AX、EAX 或 RAX 寄存器中的值與第一個操作數(目标操作數)進行比較。如果兩個值相等,則将第二個操作數(源操作數)加載到目标操作數中。 這一句的描述,也符合我們 CAS 的定義。

現在最關鍵的問題是,這裡有 4 個寄存器,我們怎麼才能知道走的是 EAX 寄存器呢?

java複制代碼Accumulator = AL, AX, EAX, or RAX depending on whether a byte, word, doubleword, or quadword comparison is being performed

// 翻譯
累加器 = AL、AX、EAX 或 RAX,具體取決于執行的是位元組、字、雙字還是四字比較 
           

這裡我們看到了,通路不同模式的記憶體單元,走的寄存器是不同的:

  • byte:AL
  • word:AX
  • doubleword:EAX
  • quadword:RAX

所有,由于上面我們使用的是 doubleword,是以這一段代碼:cmpxchg dword ptr [edx], ecx ,可以描述為:

将 EAX 寄存器中的值與 記憶體位址為 dest 的雙字記憶體單元 進行比較。如果兩個值相等,則将 ecx寄存器中的值 加載到目标操作數中。

這裡,也就正式完成了 CAS 的操作。

五、總結

魯迅先生曾說:獨行難,衆行易,和志同道合的人一起進步。彼此毫無保留的分享經驗,才是對抗網際網路寒冬的最佳選擇。

其實很多時候,并不是我們不夠努力,很可能就是自己努力的方向不對,如果有一個人能稍微指點你一下,你真的可能會少走幾年彎路。

作者:愛敲代碼的小黃

連結:https://juejin.cn/post/7241875961131466807

繼續閱讀