十一、kotlin的協程 - 快取、volatile、記憶體屏障和cas(四) --- 跑題篇

語言: CN / TW / HK

theme: cyanosis

本章寫著寫著就跑題了, 又不捨得刪除, 新手看 # 協程的共享變數安全問題簡單入門## volatile 不保證原子性部分程式碼, 其他可以不看, 太亂, 也沒用

協程的共享變數安全問題簡單入門

在使用 kotlin 的協程庫中, 我們會看到很多的 協程排程器 , 如果新增上Thread.currentThread() 函式的話, 我們會看到一些協程的背後還涉及了多執行緒, 只要有多執行緒就會存在多執行緒競爭共享變數的問題

kotlin @Test fun test01() = runBlocking<Unit> { launch { // Thread[main @coroutine#2,5,main] println("${Thread.currentThread()} launch1 正在執行 2") } launch { // Thread[main @coroutine#3,5,main] println("${Thread.currentThread()} launch2 正在執行 2") } withContext(Dispatchers.IO) { // Thread[DefaultDispatcher-worker-1 @coroutine#1,5,main] println("${Thread.currentThread()} withContext 正在執行 3") } }

你會看到上面的程式碼使用了兩個執行緒 Thread[main]Thread[DefaultDispatcher-worker-1]

協程用了三個 @coroutine#1@coroutine#2@coroutine#3 但協程 @coroutine#3 在不同的執行緒中

我們現在分別在 @coroutine#1@coroutine#2 間各自執行 10000i++ 判斷下是否執行緒安全

然後在@coroutine#2@coroutine#3 兩個執行緒間各自執行 10000i++

協程間:

kotlin @Test fun test02() = runBlocking<Unit> { var i = 0 val list = mutableListOf<Job>() repeat(10000) { list.add(launch { i++ }) list.add(launch { i++ }) } list.forEach { it.join() } println(i) // 20000 }

執行緒間:

kotlin @Test fun test03() = runBlocking<Unit> { var i = 0 val list = mutableListOf<Job>() repeat(10000) { list.add(launch { i++ }) list.add(launch(Dispatchers.IO) { i++ }) } list.forEach { it.join() } println(i) // 19668 }

可以看的出來還是存線上程安全問題, 而且協程的執行緒安全問題還更加不可預知, 用多執行緒的話, 我們都知道, 它一定執行緒不安全, 但使用的協程, 無法判斷到底是不是同一個執行緒, 這時候就需要主動的打印出來到底是哪個執行緒需要上鎖

此時沒辦法, 我們就可以去協程庫裡找找, 有沒有那種專屬的鎖

image.png

發現還真有一個鎖, 用用看看

kotlin @Test fun test04() = runBlocking<Unit> { val mutex = Mutex() var i = 0 val list = mutableListOf<Job>() repeat(10000) { list.add(launch { mutex.withLock { i++ } }) list.add(launch(Dispatchers.IO) { try { mutex.lock() i++ } finally { mutex.unlock() } }) } list.forEach { it.join() } println(i) }

注意看上面程式碼, mutex 的兩種用法

好了至此簡單的入門結束了

volatile 關鍵字

volatilejava 多執行緒中的作用有 1. 防止程式碼重排序 2. flushcpustore buffer(寫) 和 Invalidate queue(讀) 保證變數在多執行緒間可見

javavolatile 底層實現藉助了 cpumemeny barrier 記憶體屏障

store buffer 和 invalidate queue

在瞭解 store bufferinvalidate queue 是什麼之前需要了解別的知識...

cpu快取記憶體

image.png

cpu速度太快了, cpucpu行一次滴答作為時間單位, 主記憶體一次操作需要幾百次的cpu滴答

所以 cpu 不得不用 快取記憶體的方式提高整體的執行效率

下圖的時鐘週期是假設的速度比例 image.png

可以看出越接近 cpu 核心的快取速度越快, 最後到暫存器

出現快取記憶體之後, cpu可以把經常使用的變數快取到 快取中, 核心與核心之間共有的資料存放到 L3 中, 如果快取未命中 , 則需要 lock 匯流排, 去主記憶體讀取相應的變數, 存放到快取中

現在有了快取, cpu 的侷限不再是 主記憶體了, 但卻出現了新的問題, 在 核心 和 核心 之間的快取怎麼解決不一致的問題

多核心快取一致方案: MESI

多核心存在的問題

現有一變數a核心A核心B 共享, 兩核心同時修改變數a 的值, 該變數a到底應該選哪個核心的值? 還有, 如果 a 變數 的值被 核心B 修改了, 核心A 不知道變數a的值是否被修改, 導致執行緒去 核心A 讀取資料時, 讀取到舊值, 導致整個 cpu快取記憶體同一個變數a的值不一致

不過在提出方案前我們需要一下預備知識

快取行

cpu操作快取不是一個位元組一個位元組的操作, 因為這樣很慢, 訪問快取記憶體的次數也變多了, 效率很低, 於是他們定義了快取行這概念, 讓[1]核心一行一行的操作, 每一行的大小一般是 64byte(也有32byte, 128byte等)

[1]: 實際上現在的cpu未必是一行一行操作了, 可能一次性操作多行

雖然提出了快取行作為 cache 的單位, 但會出現新的問題

快取行偽共享

我們發現, 一個 java long 大小就8位元組了, 多儲存幾個變數, 會出現一種情況

變數 a b c d 在同一行緩存行儲存, 如果cpu收到變數 ainvalidate 訊息將一個變數標記為 invalid, 但不行啊, cpu操作快取的最小單位是快取行, 他會把那一行都標記為 invalid, 這樣就出問題了, b c d 都一起被殃及無辜了

所以一般情況下, 我們可以在變數a後面新增佔位變數, 讓變數 a 在單獨一行, 就可以提高效率了

把涉及多執行緒共享變數儲存在單獨的一行可以提高效率, 如果不是則沒必要

java 8 提供了註解實現 @sun.misc.Contended 上面功能, 但 java11 之後該註解被放在另一個包裡了@jdk.internal.vm.annotation.Contended, 如果要使用它需要新增-XX:-RestrictContended引數

預備知識講完了

MESI 是什麼?

為了解決多核心之間快取不一致, 業界提出了 MESI(Modified-Exclusive-Shared-Invalid) 方案, 該方案類似於讀寫鎖, 寫時獨佔, 讀時共享, 而MESI的操作單位是 快取行

MESI每一個單詞的解釋

M修改(Modified): 程式修改核心A快取中的變數a, 將快取中的變數a標記為 M, 表示該值只有該核心A剛剛修改, 而其他 核心 並不知道已經修改了, 也不知道該快取的變數已經失效了, 此時快取的資料和記憶體不同

E獨佔(Exclusive): 變數修改後, 核心A發出 invalidate 訊息給其他核心, 其他核心傳送 invalid ack核心A 之後, 核心A將該變數設定為 E 獨佔模式, 此時資料和記憶體一致, 且僅存在該快取中

S共享(share): 當核心B要讀取變數a時, 發現 ainvalid狀態, remote read 核心a 快取中的變數, 此時快取變數和記憶體一致

I失效(invalid): 核心將 invalidate queue 中的元素處理掉, 就會將部分快取行標記為 invalid, 表示該快取行失效

MESI之間的變換, 具體可以看下圖

image.png

核心發起標記訊息藉助訊息匯流排傳遞給其他核心, 而大體訊息型別可以分為下面幾種: - Read :帶上資料的實體記憶體地址發起的讀請求訊息 - Read ResponseRead 請求的響應資訊,內部包含了讀請求指向的資料 - Invalidate:該訊息包含資料的記憶體實體地址,意思是要讓其他如果持有該資料快取行的 CPU 直接失效對應的快取行 - Invalidate AcknowledgeCPUInvalidate 訊息的響應,目的是告知發起 Invalidate 訊息CPU,這邊已經失效了這個快取行啦 - Read Invalidate:這個訊息其實是 ReadInvalidate 的組合訊息,與之對應的響應自然就是一個Read Response 和 一系列的 Invalidate Acknowledge - Writeback:該訊息包含一個實體記憶體地址和資料內容,目的是把這塊資料通過匯流排寫回記憶體裡

新問題

核心A 修改變數a的值 a = 2 此時 核心A的快取行a變數被修改, 核心A將傳送 invalid 訊息藉助訊息匯流排給其他核心快取中的變數a, 高速其他核心該變數已經失效, 其他核心需要回復 invalid ack 進行應答, 應答完畢後 核心A 開始其他操作, 有沒有發現這中間出現了新的問題????

核心A 發出invalid訊息, 一致等待(空等期)?!!! 直到收到其他核心的 invalid ack 訊息才會重新執行下一個指令???

所以 store buffer 誕生了, 還是原先的 加個 萬能中間層 解決問題

storebuffer

image.png

有了storebuffer , 核心A 再也不用等著, 直接把修改丟給 store buffer , 同時給其他核心傳送invalid訊息, 自己則不需要等待 ack , 可以做其他事情, 等到其他核心ack回覆後, 核心A 讀取 store buffer 裡的資料, 將其移動到 cache line , 這樣一個同步等待事件, 變成了一個非同步事件

同步等待, 變成了非同步

新問題

引入 store buffer 確實讓 核心 的利用率變高了, 但同時有多了個問題

核心A變數a的修改拋入 store buffer 後, 在收到ack前再次讀取 變數a 的值, 會發現 變數a 還是舊值

``` a = 1 funA { a = 2 }

funB { if a == 2 { // xxxxxx } } ```

核心A 執行了 funA變數 a 改為 2, 然後立即執行 funB 判斷a == 2 此時居然是 false, 這明顯不對

注意這是單核的情況, 單核都會出現這樣的問題, 炸裂了

Store Forwarding: 先從 store buffer 讀起

為了解決這個問題, 工程師引入了新的概念, 叫 Store Forwarding, 很簡單, 先讀 store buffer 內的資料再讀快取唄

現在單核心的問題解決了, 多核心又炸了

``` a = b = 0 funA () { a = 1 b = 1 }

funB() { while (b == 0) continue; assert(a == 1) } ```

現在有這麼一個場景, a核心AB 共同持有, 而b 只有核心A 擁有, 核心A 執行 funA, 核心B執行 funB

  1. 首先 a = 1 , 核心A 將 修改丟給 store buffer , 併發送 invalid 訊息 2.b = 1, 核心A 直接將快取的b修改為1(b是獨佔的, 不需要傳送invalid msg給其他核心)
  2. 核心B 快取中沒有, 發出 remote read 操作快取中找到 b = 1, 執行 while 判斷, 不滿足跳出迴圈
  3. 核心B 斷言 a == 1 , 但此時會丟擲異常, 因為 核心A還沒有收到 invalid ack訊息, 所以預設還是 a == 0

解決方案便是新增記憶體屏障

記憶體屏障

記憶體屏障是一種同步屏障指令, 在記憶體屏障前後的程式碼不會重排序, 嚴格按照一定的順序來執行, 也就是說在記憶體屏障之前的指令和之後的指令不會由於系統優化原因而導致亂序

我們只要把程式碼改成這樣:

``` a = b = 0 funA () { a = 1 smp_wmb() // linux 對寫記憶體屏障的封裝 b = 1 }

funB() { while (b == 0) continue; assert(a == 1) } ```

新增寫記憶體屏障後,對變數a, 甚至前面的變數寫入都會被寫入到快取中, 寫記憶體屏障主要針對的是 store buffer, 新增寫記憶體屏障後, store buffer 將會被 flush 掉, 裡面的變數全部被寫入到快取中, 這樣, 另一個核心讀取該變數時, 就可以直接remote read 該變數, 直接從快取中讀取

注意, 前面的 Store Forwarding 針對的是單核程式碼重排序的情況, 不是多核

但... 還有問題

invalidate queues

新問題: store buffer 不夠用怎麼辦???

現在一個新的問題是, store buffer 不夠大, 快取上一堆 miss 導致 核心 不斷的把變數寫入到 store buffer 中, store buffer 告急, 核心又得空等, 等到 store buffer 清空後才能繼續處理其他邏輯, 解決方案很簡單, 縮短 變數 在 store buffer 中的停留時間

我們再分析下前面的邏輯, 找找, 哪個步驟讓變數停留在 store buffer 的時間變長

核心寫入 store buffer 發出 invalid 訊息, 核心做其他處理, 等到 ack 後 再將 store buffer 寫入到快取中(等到ack後也未必會立即重新整理到快取中, 這跟 Thread.start 一個執行緒一樣,未必馬上就能夠啟動)

而我們現在遇到的問題是store buffer 不夠用, 很明顯, 前面的邏輯中, 等到 ack 後 這步驟直接影響了 變數 在 store buffer 中停留的時間

目前的解決方案是新增invalidate queues , 主要功能是儲存來自其他核心的 invalid 訊息, 咦? 這不是還沒解決麼?

再屢屢, 站在收到 invalid 訊息的核心角度看, 如果我收到 invalid 訊息後, 需要找到快取中的某個快取行, 將其標記為 invalid 狀態, 標記完成後, 發出 ack 訊息

誒? 又是同步操作了不是? 你想想, 萬一其他核心的cache瘋狂的修改一堆變數, 作為收到invalid訊息的核心來說, 得多痛苦, 一收到訊息, 它就得去標記快取行, 發出ack , 一堆訊息它也馬上去標記快取行, 再發出 ack , 我核心不幹其他活啦?

那為什麼不一收到 invalid 訊息, 把該訊息存入 invalidate queue中, 然後直接發出 ack, 等到我想處理 invalidate queue 的時候再去一個一個讀取出來, 在快取中找到變數標記invalid, 雙贏?

這項功能少了找快取行中某個變數, 和標記該變數的時間, 換成 queue.add(message) 的時間

別高興太早, 又有新問題產生了

又遇新問題

現在我們再屢屢, invalidate queue 的出現使得失效變數在快取被標記的時間延後了, 這樣有個新的問題

我讀你, 咋辦???

具體看看下面程式碼

a = b = 0 funA() { a = 1 smp_wmb() // linux 對寫記憶體屏障的封裝 b = 1 } funB() { while (b == 0) continue; assert(a == 1) }

還是前面的條件, a變數核心(A B 核心) 共有, b 變數只有 核心A

  1. 核心A 執行 funA, a = 1 存入 store buffer 發出 invalid 訊息給其他核心
  2. 核心B收到 invalid 訊息, 把訊息存入 invalidate queue 然後立即發出 ack 訊息
  3. 核心A遇到 寫記憶體屏障變數 a 寫入到 快取中 4.核心A執行 b=1 因為是 核心A 獨佔的變數, 所以可以直接寫入到快取中 5.核心B發現 b == 0 ==> false , 則跳出while迴圈
  4. 核心B判斷變數 a 的狀態, 但是由於 invalid 訊息被存入queue 中了, 所以核心認為 a = 0 是正確的

那要怎麼解決呢? 難道又得效仿前面 store buffer , 讀取變數之前先去 invalidate queue 找找有沒有失效???

但實際上, 工程師並沒有選擇這樣做, 可能的原因是 invalidate queue 是佇列, 需要一個一個遍歷, 效率慢, 還有一種可能是 invalidate queue 可能會很長, 還有可能和 store forwaring 一樣, 多核間出問題怎麼解決?

這裡沒去深入, 再深入 kotlin 協程還學不學了??? 我瘋了, 寫著寫著又偏離了主題

解決方案是 加上 讀記憶體屏障

a = b = 0 funA() { a = 1 smp_wmb() // linux 對寫記憶體屏障的封裝 b = 1 } funB() { while (b == 0) continue; smp_rmb(); assert(a == 1) }

加上讀記憶體屏障, 該功能可以在讀取後面變數前, 處理完 invalid queue 然後再真正的讀取變數 a , 此時變數 a 就不再是 S 共享 狀態了, 而是 I 失效 狀態, 需要去 remote read, 讀取 變數 a

好了, 核心分析基本到這裡就行了, 分析了這麼多, 都是虛的, 我沒辦法直接分析核心(懶), 但可以分析 volatile 的原始碼

jvm底層分析 volatile 的原始碼(主要分析x86)

talk is cheap, show me the code

image.png

會發現 isVolatile 如果型別是 int 型, 會呼叫

obj->release_int_field_put(field_offset, STACK_INT(-1));

c++ inline void oopDesc::release_int_field_put(int offset, jint contents) { OrderAccess::release_store(int_field_addr(offset), contents); }

c++ inline void OrderAccess::release_store(volatile jint* p, jint v) { release(); *p = v; }

storestore --> release

c++ inline void OrderAccess::release() { WRITE_MEM_BARRIER; }

```c++

define WRITE_MEM_BARRIER __asm __volatile ("":::"memory")

```

這裡需要了解下 gcc 的指令, 需要點別的知識, 我也不太瞭解, 知道他是記憶體屏障就行了, 具體可以百度 gcc內聯彙編 + 你想要查詢的關鍵字

上面這是寫入的記憶體屏障, 而 如果是 volatile 變數的話, 在賦值結束之後還會呼叫:

storeload --> fence

c++ inline void OrderAccess::storeload() { fence(); }

```c++ inline void OrderAccess::fence() {

ifdef AMD64

StubRoutines_fence();

else

// 判斷是不是多核心 if (os::is_MP()) { __asm { lock add dword ptr [esp], 0; } }

endif // AMD64

} ```

這裡我們會發現 volatile 有兩個記憶體屏障 一個是 OrderAccess::release 另一個是 OrderAccess::storeload

同時我們發現了很多記憶體屏障

java 是個記憶體屏障

c++ inline void OrderAccess::loadload() { acquire(); } inline void OrderAccess::storestore() { release(); } inline void OrderAccess::loadstore() { acquire(); } inline void OrderAccess::storeload() { fence(); }

然後會發現 OrderAccess::release 其實 就是 OrderAccess::storestore

所以 volatile 前後使用的記憶體屏障是 storestorestoreload

storestore 使用的是 C語言 原生的 volatile , 這裡可以查下 c++volatile 的功能:

volatile關鍵字用來阻止(偽)編譯器認為的無法“被程式碼本身”改變的程式碼(變數/物件)進行優化。如在C語言中,volatile關鍵字可以用來提醒編譯器它後面所定義的變數隨時有可能改變,因此編譯後的程式每次需要儲存或讀取這個變數的時候,都會直接從變數地址中讀取資料。

不要給 C語言volatile 新增太多的功能了, 它實際上只有一個功能, 防止編譯器優化, 網路上很多人給 C語言volatile 加了很多不屬於它的能力, 看呆了...

storeloadx86 支援的系統原語, 但是開銷極大, 使用的是 lock指令 執行, 鎖住了快取或者cpu匯流排

loadloadloadstore 都呼叫的 acquire, 那 acquire 原始碼呢?

loadload loadstore --> acquire

```c++ inline void OrderAccess::acquire() { // 如果是 amd 的系統

ifndef AMD64

__asm { mov eax, dword ptr [esp]; }

endif // !AMD64

} ```

volatile的原始碼在: bytecodeInterpreter.cpp 檔案, 而 四個 java 的記憶體屏障在 orderAccess_windows_x86.inline.hpp 這裡我選擇window x86 環境下的四個記憶體屏障實現方式, 其他檔案看

image.png

volatile 不保證原子性

volatile 保證可見性和防止程式碼重排序外, 就沒別的功能了

很多人就會覺得不對啊, volatile 不是還 保證原子性 麼?

相比很多人第一時間想到的是這樣一段程式碼 :

```kotlin @Volatile var flag = false

var a = 0

fun funA() { TimeUnit.MILLISECONDS.sleep(1555) /* * 寫記憶體屏障,清空store buffer , 這樣不會存在未寫入快取的變數, 其他核心也能讀取到資料 / // storestore flag = true // storeload a = 1 }

fun funB() { // loadload while (!flag) { continue } // loadstore /* * 上面那個記憶體屏障,直接清空了 invalidate queue,所以 a 的值被標記為 invalid 狀態 * 這樣,下面的程式碼可讀了,至少不會讀取到假的變數, 核心回去 remote read 遠端 * 的核心 / assert(a == 1) log("funB running...") }

@Test fun test01() = runBlocking { val job1 = launch(Dispatchers.IO) { funA() } val job2 = launch(Dispatchers.Unconfined) { funB() } joinAll(job1, job2) } ```

這段程式碼展示了 kotlin 的 volatile 的用法: @Volatile

你看這不是原子操作麼? 實際上, 則僅僅是可見性和防止重排序問題

如果把 flag 變成 flag++ 的話, 就不一樣了

誒, 我們前面寫過類似的程式碼

kotlin @Test fun test03() = runBlocking<Unit> { var i = 0 val list = mutableListOf<Job>() repeat(10000) { list.add(launch { i++ }) list.add(launch(Dispatchers.IO) { i++ }) } list.forEach { it.join() } println(i) // 19668 }

改下試試

```kotlin @Volatile var i = 0

@Test fun test01() = runBlocking { val list = mutableListOf() repeat(10000) { list.add(launch { i++ }) list.add(launch(Dispatchers.IO) { i++ }) } list.forEach { it.join() } println(i) // 19904 } ```

結果是 19904

為什麼? 其實很簡單, flag = true 編譯成位元組碼後, 只有一句, 而改成 i++ 的話, 程式碼就變成了 i = i + 1, 這樣就個3步驟: 1. 讀取 i 2. i + 1 3. 把值賦值給 i

三個步驟, 明顯不是執行緒安全的

```kotlin @Volatile var i = 0 val mutex = Mutex()

@Test fun test01() = runBlocking { val list = mutableListOf() repeat(10000) { list.add(launch { mutex.withLock { i++ } }) list.add(launch(Dispatchers.IO) { mutex.withLock { i++ } }) } list.forEach { it.join() } println(i) // 19668 } ```

當然這不是唯一的解決方案, 我們還可以使用無鎖casAtomicInterger 解決

```kotlin @Volatile var i: AtomicInteger = AtomicInteger(0)

@Test fun test01() = runBlocking { val list = mutableListOf() repeat(10000) { list.add(launch { i.getAndIncrement() }) list.add(launch(Dispatchers.IO) { i.getAndIncrement() }) } list.forEach { it.join() } println(i) // 20000 } ```

在 cas 底下, 我們有 三個 值, 舊值, 新值和實際值

1. 舊值(也可以叫預估值): 剛剛讀取出來的值
2. 新值: 是我們需要設定進入的值
3. 實際值: 是我們主存裡的值(通常是 volatile修飾的變數)

如果需要設定新的值, 首先 判斷 舊值 和 實際值 是否相同?

如果相同, 則直接把 新 的值 設定進去

如果不相同, 說明在這期間, 值已經被修改了, 則再次讀下 實際值 的值, 把該值作為舊值, 然後從 判斷舊值和實際值是否相等 開始迴圈, 直到將值設定進去

讀取出來的舊值判斷舊值和實際值是否相等之間有時差, cas使用上了這份時差, 只要在這時差之中, 舊值和實際值相同, 我們就可以立馬將新值設定到實際值中

來, 我們簡單分析下 AtomicInteger 的原始碼把這三個值找出來

image.png

這裡設定了值, 這裡的 value 被修飾成 volatile , 所以是 實際值

image.png

現在我們找舊值

java public final int getAndIncrement() { return U.getAndAddInt(this, VALUE, 1); }

這裡看不出來, 往getAndAddInt函式裡頭走

java @HotSpotIntrinsicCandidate // o: 是物件 // offset: 是物件所處 value 的偏移地址 // 上面這倆配合能夠拿到 value 實際值 的值 // delta: 這是增加的值, 是新值的增量 public final int getAndAddInt(Object o, long offset, int delta) { int v; do { // 拿到舊值 v = getIntVolatile(o, offset); // 對比下, o + offset 組成的 實際值是否和 舊值 v 相等, 如果相等, 直接設定 v + delta 新的值 } while (!weakCompareAndSetInt(o, offset, v, v + delta)); return v; }

舊值 v, 實際值 o + offset, 新值 v + delta

話說 cas jvm原始碼不用看了吧? 算了還是簡單看下

AtomicInteger 開始深入 jvm 底層分析 cas 原始碼

Unsafe.java 檔案下有這麼一個函式

java public final native boolean compareAndSetInt(Object o, long offset, int expected, int x);

從這裡查起, 然後我崩了, 執行的jdk版本是 openJDK 11, 原始碼的版本是 openJDK1.8, 好像原始碼優點不太一樣???

換了下 jdk 版本果然

java public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x);

然後就找到了原始碼:

unsafe.cpp

c++ UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x)) UnsafeWrapper("Unsafe_CompareAndSwapInt"); // 把我們 java 的o當作自己強轉成(oop*)然後再取值 *(oop*) 指標 oop p = JNIHandles::resolve(obj); // 把 p + offset 偏移值, 得到 addr 指標 jint *addr = (jint *)index_oop_from_field_offset_long(p, offset); // 重點在這裡 // 對比並交換, x 是我們新值, addr 是實際值, e 是舊值(預估值expected) return (jint)(Atomic::cmpxchg(x, addr, e)) == e; UNSAFE_END

看這個 jobject obj, jlong offset, jint e, jint x, 和我們java的引數配上了

jobject obj, jlong offset, jint e, jint x

Object o, long offset, int expected, int x

然後我們深入到 Atomic::cmpxchg 內部

image.png

我們找 window x86 檔案

會發現有兩個相同函式簽名的 cmpxchg , 別急一個是 AMD 的, 我們找 intel 的,

c++ inline jint Atomic::cmpxchg(jint exchange_value, volatile jint* dest, jint compare_value) { 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 } }

又到了看不太懂的彙編環節, 看的出來底層使用的就是 彙編程式碼 cmpxchg, 如果是多核的還給上了鎖 LOCK_IF_MP(mp)

底層變數名字寫的真清楚啊, exchange_value 用於交換的值, dest 源於哪個值的指標, compare_value 需要比較的值

剩下彙編, 看的懂一點, 但 cmpxchg 有什麼特性就不太懂了, 想更深入的, 自行百度

又 6000 字了, 強迫症, 先把文章發了吧, 等有空再整理整理(可能有錯), 話說這章跟 kotlin 有關係麼??? !!!