十一、kotlin的協程 - 快取、volatile、記憶體屏障和cas(四) --- 跑題篇
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
間各自執行 10000
次 i++
判斷下是否執行緒安全
然後在@coroutine#2
和 @coroutine#3
兩個執行緒間各自執行 10000
次 i++
協程間:
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
}
可以看的出來還是存線上程安全問題, 而且協程的執行緒安全問題還更加不可預知, 用多執行緒的話, 我們都知道, 它一定執行緒不安全, 但使用的協程, 無法判斷到底是不是同一個執行緒, 這時候就需要主動的打印出來到底是哪個執行緒需要上鎖
此時沒辦法, 我們就可以去協程庫裡找找, 有沒有那種專屬的鎖
發現還真有一個鎖, 用用看看
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 關鍵字
volatile
在 java
多執行緒中的作用有
1. 防止程式碼重排序
2. flush
了 cpu
的 store buffer
(寫) 和 Invalidate queue
(讀) 保證變數在多執行緒間可見
java
的 volatile
底層實現藉助了 cpu
的 memeny barrier 記憶體屏障
store buffer 和 invalidate queue
在瞭解 store buffer
和 invalidate queue
是什麼之前需要了解別的知識...
cpu快取記憶體
cpu
速度太快了, cpu
以 cpu
行一次滴答作為時間單位, 主記憶體一次操作需要幾百次的cpu
滴答
所以 cpu
不得不用 快取記憶體的方式提高整體的執行效率
下圖的時鐘週期是假設的速度比例
可以看出越接近 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收到變數 a 的 invalidate 訊息將一個變數標記為 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時, 發現 a
是 invalid
狀態, remote read
核心a 快取中的變數, 此時快取變數和記憶體一致
I失效(invalid): 核心將 invalidate queue
中的元素處理掉, 就會將部分快取行標記為 invalid
, 表示該快取行失效
MESI
之間的變換, 具體可以看下圖
核心發起標記訊息藉助訊息匯流排傳遞給其他核心, 而大體訊息型別可以分為下面幾種:
- Read
:帶上資料的實體記憶體地址發起的讀請求訊息
- Read Response
:Read
請求的響應資訊,內部包含了讀請求指向的資料
- Invalidate
:該訊息包含資料的記憶體實體地址,意思是要讓其他如果持有該資料快取行的 CPU 直接失效對應的快取行
- Invalidate Acknowledge
:CPU
對Invalidate 訊息
的響應,目的是告知發起 Invalidate 訊息
的CPU
,這邊已經失效了這個快取行啦
- Read Invalidate
:這個訊息其實是 Read
和 Invalidate
的組合訊息,與之對應的響應自然就是一個Read Response
和 一系列的 Invalidate Acknowledge
- Writeback
:該訊息包含一個實體記憶體地址和資料內容,目的是把這塊資料通過匯流排寫回記憶體裡
新問題
核心A
修改變數a
的值 a = 2
此時 核心A
的快取行a變數
被修改, 核心A
將傳送 invalid 訊息
藉助訊息匯流排
給其他核心快取中的變數a
, 高速其他核心該變數已經失效, 其他核心需要回復 invalid ack
進行應答, 應答完畢後 核心A
開始其他操作, 有沒有發現這中間出現了新的問題????
核心A
發出invalid訊息
, 一致等待(空等期)?!!! 直到收到其他核心的 invalid ack 訊息
才會重新執行下一個指令???
所以 store buffer
誕生了, 還是原先的 加個 萬能中間層
解決問題
storebuffer
有了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
是核心A
和 B
共同持有, 而b
只有核心A
擁有, 核心A
執行 funA
, 核心B
執行 funB
- 首先
a = 1
,核心A
將 修改丟給store buffer
, 併發送invalid 訊息
2.b = 1
,核心A
直接將快取的b
修改為1
(b
是獨佔的, 不需要傳送invalid msg
給其他核心) 核心B
快取中沒有, 發出remote read
操作快取中找到b = 1
, 執行while
判斷, 不滿足跳出迴圈核心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
有
核心A
執行funA
,a = 1
存入store buffer
發出invalid 訊息
給其他核心核心B
收到invalid 訊息
, 把訊息存入invalidate queue
然後立即發出ack 訊息
核心A
遇到寫記憶體屏障
將變數 a
寫入到 快取中 4.核心A
執行b=1
因為是核心A
獨佔的變數, 所以可以直接寫入到快取中 5.核心B
發現b == 0 ==> false
, 則跳出while迴圈
核心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
會發現 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
前後使用的記憶體屏障是 storestore
和 storeload
而 storestore
使用的是 C語言 原生的 volatile
, 這裡可以查下 c++
的 volatile
的功能:
volatile關鍵字用來阻止(偽)編譯器認為的無法“被程式碼本身”改變的程式碼(變數/物件)進行優化。如在C語言中,volatile關鍵字可以用來提醒編譯器它後面所定義的變數隨時有可能改變,因此編譯後的程式每次需要儲存或讀取這個變數的時候,都會直接從變數地址中讀取資料。
不要給
C語言
的volatile
新增太多的功能了, 它實際上只有一個功能, 防止編譯器優化, 網路上很多人給C語言
的volatile
加了很多不屬於它的能力, 看呆了...
storeload
是 x86
支援的系統原語, 但是開銷極大, 使用的是 lock指令
執行, 鎖住了快取或者cpu匯流排
loadload
和 loadstore
都呼叫的 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 環境下的四個記憶體屏障實現方式, 其他檔案看
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
結果是 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
當然這不是唯一的解決方案, 我們還可以使用無鎖cas
的 AtomicInterger
解決
```kotlin @Volatile var i: AtomicInteger = AtomicInteger(0)
@Test
fun test01() = runBlocking
在 cas 底下, 我們有 三個 值, 舊值, 新值和實際值
1. 舊值(也可以叫預估值): 剛剛讀取出來的值
2. 新值: 是我們需要設定進入的值
3. 實際值: 是我們主存裡的值(通常是 volatile
修飾的變數)
如果需要設定新的值, 首先 判斷 舊值 和 實際值 是否相同?
如果相同, 則直接把 新 的值 設定進去
如果不相同, 說明在這期間, 值已經被修改了, 則再次讀下 實際值
的值, 把該值作為舊值
, 然後從 判斷舊值和實際值是否相等
開始迴圈, 直到將值設定進去
讀取出來的舊值
和判斷舊值和實際值是否相等
之間有時差
,cas
使用上了這份時差, 只要在這時差之中, 舊值和實際值相同, 我們就可以立馬將新值設定到實際值中
來, 我們簡單分析下 AtomicInteger
的原始碼把這三個值找出來
這裡設定了值, 這裡的 value
被修飾成 volatile
, 所以是 實際值
現在我們找舊值
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
內部
我們找 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 有關係麼??? !!!