本章写着写着就跑题了, 又不舍得删除, 新手看
# 协程的共享变量安全问题简单入门
和## volatile 不保证原子性
部分代码, 其他可以不看, 太乱, 也没用
协程的共享变量安全问题简单入门
在使用 kotlin
的协程库中, 我们会看到很多的 协程调度器 , 如果添加上Thread.currentThread()
函数的话, 我们会看到一些协程的背后还涉及了多线程, 只要有多线程就会存在多线程竞争共享变量的问题
@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++
协程间:
@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
}
线程间:
@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
}
可以看的出来还是存在线程安全问题, 而且协程的线程安全问题还更加不可预知, 用多线程的话, 我们都知道, 它一定线程不安全, 但使用的协程, 无法判断到底是不是同一个线程, 这时候就需要主动的打印出来到底是哪个线程需要上锁
此时没办法, 我们就可以去协程库里找找, 有没有那种专属的锁
发现还真有一个锁, 用用看看
@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
多线程中的作用有
- 防止代码重排序
-
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
状态, 其他核心标记完毕后需要回复 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
不够大, 执行一堆变量的修改导致 核心 不断的把变量写入到 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
, 双赢?
这项功能让核心 ack 的时间从找缓存行中某个变量, 和标记该变量的时间, 换成
queue.add(message)
的时间, 核心只要add
下, 就马上ack
了
别高兴太早, 又有新问题产生了
又遇新问题
现在我们再屡屡, 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(); // linux 对读内存屏障的封装
assert(a == 1)
}
加上读内存屏障
, 该功能可以在读取后面变量前, 处理完 invalid queue
然后再真正的读取变量 a
, 此时变量 a
就不再是 S 共享
状态了, 而是 I 失效
状态, 需要去 remote read
, 读取 变量 a
好了, 核心分析基本到这里就行了, 分析了这么多, 都是虚的, 我没能力直接分析内核, 但可以分析
volatile
的源码
分析 volatile
在jvm
中的源码(主要分析x86
)
talk is cheap, show me the code
众所肘子, java
中经常说 写入 volatile
变量 时会在写入前加上 storestore
写入后加上 storeload
, 但 x86
除外, 现在我们来看下, x86
如何除外的?
找到 x86
判断 volatile
的源码位置
会发现 isVolatile
如果类型是 int 型
, 会调用
obj->release_int_field_put(field_offset, STACK_INT(-1));
inline void oopDesc::release_int_field_put(int offset, jint contents)
{
// 又是熟悉的根据 offset 偏移量查找变量地址的方式
OrderAccess::release_store(int_field_addr(offset), contents);
}
inline void OrderAccess::release_store(volatile jint* p, jint v) {
*p = v;
}
x86
下,Java
的volatile
变量的写入前的 内存屏障是空的, 仅仅用了 C语言的volatile
关键字C语言的volatile关键字用来阻止(伪)编译器认为的无法“被代码本身”改变的代码(变量/对象)进行优化。如在C语言中,volatile关键字可以用来提醒编译器它后面所定义的变量随时有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,都会直接从变量地址中读取数据。
不要给
C语言
的volatile
添加太多的功能了, 它实际上只有一个功能, 防止编译器优化, 从变量地址中读取变量, 网络上很多人给C语言
的volatile
加了很多不属于它的能力, 看呆了…结论 写入
volatile
之前的内存屏障 无, 而且还不是调用的storestore
, 是release_store
那真正的x86
的storestore
呢?
storestore --> release
在 orderAccess_windows_x86.inline.hpp
文件中可以看到
inline void OrderAccess::storestore() {
release();
}
inline void OrderAccess::release() {
// A volatile store has release semantics.
volatile jint local_dummy = 0;
}
还是没做内存屏障,
x86
挺特殊的
在别的核心架构里就做了内存屏障 orderAccess_linux_zero.inline.hpp
inline void OrderAccess::release_store(volatile jint* p, jint v) {
release();
*p = v;
}
inline void OrderAccess::release() {
WRITE_MEM_BARRIER;
}
#define WRITE_MEM_BARRIER __asm __volatile ("":::"memory")
这里需要了解下
gcc
的指令, 需要点别的知识, 我也不太了解, 知道他是内存屏障就行了, 具体可以百度gcc内嵌汇编 + 你想要查询的关键字
那么写volatile
变量之后的内存屏障呢?
还真是 storeload
storeload --> fence
inline void OrderAccess::storeload() {
fence();
}
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_store
另一个是 OrderAccess::storeload
, 跟书本上常说的 volatile
写入前后的内存屏障, 大概也许一摸一样, 因为用的 C语言的 volatile
防止编译器优化
好吧, 结论是
x86
核心下volatile
写入前就没屏障, 写入后加storeload
屏障, 使用的还是lock
指令
同时我们发现了很多内存屏障
java 四个内存屏障
inline void OrderAccess::loadload() { acquire(); }
inline void OrderAccess::storestore() { release(); }
inline void OrderAccess::loadstore() { acquire(); }
inline void OrderAccess::storeload() { fence(); }
storeload
是 x86
仅支持的系统原语, 但是开销极大, 使用的是 lock指令
执行, 锁住了缓存或者cpu总线
loadload loadstore --> acquire
inline void OrderAccess::acquire() {
// 如果是 amd 的系统
#ifndef AMD64
__asm {
mov eax, dword ptr [esp];
}
#endif // !AMD64
}
惊了, 好像啥事没干, 对, x86 就没上锁, 哈哈, 我看了下其他核心架构上的代码, 上内存屏障了
书本上也是这么说的, x86
仅仅实现了 storeload
对上了
volatile的源码在:
bytecodeInterpreter.cpp
文件, 而 四个 java 的内存屏障在orderAccess_windows_x86.inline.hpp
这里我选择window x86 环境下的四个内存屏障实现方式, 其他文件看
我选了 orderAccess_linux_zero.inline.hpp
简单的看了看
#define READ_MEM_BARRIER __asm __volatile ("":::"memory")
#define WRITE_MEM_BARRIER __asm __volatile ("":::"memory")
#define FULL_MEM_BARRIER __sync_synchronize()
inline void OrderAccess::loadload() { acquire(); }
inline void OrderAccess::storestore() { release(); }
inline void OrderAccess::loadstore() { acquire(); }
inline void OrderAccess::storeload() { fence(); }
inline void OrderAccess::acquire() {
READ_MEM_BARRIER;
}
inline void OrderAccess::release() {
WRITE_MEM_BARRIER;
}
inline void OrderAccess::fence() {
FULL_MEM_BARRIER;
}
volatile 不保证原子性
volatile
保证可见性和防止代码重排序外, 就没别的功能了
很多人就会觉得不对啊, volatile
不是还 保证原子性 么?
相比很多人第一时间想到的是这样一段代码 :
@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++
的话, 就不一样了
诶, 我们前面写过类似的代码
@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
}
改下试试
@Volatile
var i = 0
@Test
fun test01() = runBlocking<Unit> {
val list = mutableListOf<Job>()
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步骤:
- 读取
i
i + 1
- 把值赋值给
i
三个步骤, 明显不是线程安全的
@Volatile
var i = 0
val mutex = Mutex()
@Test
fun test01() = runBlocking<Unit> {
val list = mutableListOf<Job>()
repeat(10000) {
list.add(launch {
mutex.withLock {
i++
}
})
list.add(launch(Dispatchers.IO) {
mutex.withLock {
i++
}
})
}
list.forEach {
it.join()
}
println(i) // 19668
}
当然这不是唯一的解决方案, 我们还可以使用无锁cas
的 AtomicInterger
解决
@Volatile
var i: AtomicInteger = AtomicInteger(0)
@Test
fun test01() = runBlocking<Unit> {
val list = mutableListOf<Job>()
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
的源码把这三个值找出来
这里设置了值, 这里的 value
被修饰成 volatile
, 所以是 实际值
现在我们找旧值
public final int getAndIncrement() {
return U.getAndAddInt(this, VALUE, 1);
}
这里看不出来, 往getAndAddInt
函数里头走
@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
文件下有这么一个函数
public final native boolean compareAndSetInt(Object o, long offset, int expected, int x);
从这里查起, 然后我崩了, 运行的jdk版本是 openJDK 11
, 源码的版本是 openJDK1.8
, 好像源码有点不太一样???
换了下 jdk 1.8
版本果然
public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x);
然后就找到了源码:
unsafe.cpp
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
的, 不用看
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
有什么特性就不太懂了, 想更深入的, 自行百度
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` 的, 不用看
```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
需要比较的值