本章写着写着就跑题了, 又不舍得删除, 新手看 # 协程的共享变量安全问题简单入门## 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 间各自执行 10000i++ 判断下是否线程安全

然后在@coroutine#2@coroutine#3 两个线程间各自执行 10000i++

协程间:

@Test
fun test02() = runBlocking<Unit> {var i = 0val 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 = 0val 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 = 0val 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高速缓存

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xzNNhw12-1656300009626)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/562899a0699e4101a761abe1d41893fe~tplv-k3u1fbpfcp-watermark.image?)]

cpu速度太快了, cpucpu一次滴答作为时间单位, 主内存一次操作需要几百次的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收到变量 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之间的变换, 具体可以看下图

核心发起标记消息借助消息总线传递给其他核心, 而大体消息类型可以分为下面几种:

  • 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 状态, 其他核心标记完毕后需要回复 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 = 1b = 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 = 1smp_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 = 1smp_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 = 1smp_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 的源码

分析 volatilejvm中的源码(主要分析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 下, Javavolatile 变量的写入前的 内存屏障是空的, 仅仅用了 C语言的 volatile 关键字

C语言的volatile关键字用来阻止(伪)编译器认为的无法“被代码本身”改变的代码(变量/对象)进行优化。如在C语言中,volatile关键字可以用来提醒编译器它后面所定义的变量随时有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,都会直接从变量地址中读取数据。

不要给 C语言volatile 添加太多的功能了, 它实际上只有一个功能, 防止编译器优化, 从变量地址中读取变量, 网络上很多人给 C语言volatile 加了很多不属于它的能力, 看呆了…

结论 写入 volatile 之前的内存屏障 无, 而且还不是调用的 storestore, 是release_store 那真正的 x86storestore 呢?

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 AMD64StubRoutines_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(); }

storeloadx86 仅支持的系统原语, 但是开销极大, 使用的是 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 = falsevar a = 0fun funA() {TimeUnit.MILLISECONDS.sleep(1555)/*** 写内存屏障,清空store buffer , 这样不会存在未写入缓存的变量, 其他核心也能读取到数据*/// storestoreflag = true// storeloada = 1
}fun funB() {// loadloadwhile (!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 = 0val 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步骤:

  1. 读取 i
  2. i + 1
  3. 把值赋值给 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
}

当然这不是唯一的解决方案, 我们还可以使用无锁casAtomicInterger 解决

@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, destmov ecx, exchange_valuemov eax, compare_valueLOCK_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` 内部[外链图片转存中...(img-XqoBzqw2-1656300009654)]我们找 `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, destmov ecx, exchange_valuemov eax, compare_valueLOCK_IF_MP(mp)cmpxchg dword ptr [edx], ecx}
}

又到了看不太懂的汇编环节, 看的出来底层使用的就是 汇编代码 cmpxchg, 如果是多核的还给上了锁 LOCK_IF_MP(mp)

底层变量名字写的真清楚啊, exchange_value 用于交换的值, dest 源于哪个值的指针, compare_value 需要比较的值

剩下汇编, 看的懂一点, 但 cmpxchg 有什么特性就不太懂了, 想更深入的, 自行百度

又 6600 字了, 强迫症, 先把文章发了吧, 等有空再整理整理(可能有错), 话说这章跟 kotlin 有关系么??? !!!

十一、kotlin的协程 - 缓存、volatile、内存屏障和cas(四) --- 跑题篇相关推荐

  1. 十一、kotlin的协程(一)

    theme: Chinese-red 学习的前提 java线程需要大概知道点 协程是线程执行的任务, 协程和用户线程的区别在于, 协程背靠强大的编译器, 协程有专属于协程的调度器和一堆方便好用的函数, ...

  2. Kotlin 之 协程

    初识协程,启动取消协程,Flow异步流,协程并发 目录 (一)初识协程 协程是什么? Android中协程解决了什么问题? 协程的挂起与恢复 挂起和阻塞 协程的调度器 Dispatchers 任务泄露 ...

  3. Kotlin中协程理解与实战(一)

    Kotlin中协程理解与实战(一) 什么是协程 在Android中协程用来解决什么问题 协程是: suspend -也称为挂起或暂停,用于暂停执行当前协程,并保存所有局部变量: resume -用于让 ...

  4. 【对比Java学Kotlin】协程简史

    文章目录 一.概念释义 1.1 协程定义 1.2 与线程的关系 1.3 协程简史 二.种类划分 2.1 按调用栈分类 2.2 按调度方式分类 三.异步编程 3.1 多线程 3.2 回调 3.3 Pro ...

  5. Kotlin的协程与生命周期

    文章目录 一.前言 二.引入依赖 三.代码示例 1.基础用法 2.repeatOnLifecycle 3.flowWithLifecycle 4.lifecycle.whenCreated.lifec ...

  6. Kotlin高级协程

    Kotlin高级协程 一.前言 二.先从线程说起 三.协程的设计思想 四.协程特点:优雅的实现移步任务 五.协程基本使用 六.协程和线程相比有什么特点,如何优雅的实现异步任务 一.前言 在文章正式上干 ...

  7. Kotlin(3)-协程和操作符重载,Java程序员秋招三面蚂蚁金服

    Kotlin 文件和类不存在一对一关系 共生体 继承 修饰符 空指针问题 正文 重难点 协程 想了很久,关于协程的内容,在官网上确实有很多内容,基础知识概念,基本使用,以及 流操作,通道,异常处理,并 ...

  8. kotlin coroutines 协程教程-入门用法

    kotlin coroutines 协程教程-入门用法 Coroutine 协程,是kotlin 上的一个轻量级的线程库,对比 java 的 Executor,主要有以下特点: 更轻量级的 api 实 ...

  9. 解密Go协程的栈内存管理

    应用程序的内存会分成堆区(Heap)和栈区(Stack)两个部分,程序在运行期间可以主动从堆区申请内存空间,这些内存由内存分配器分配并由垃圾收集器负责回收.栈区的内存由编译器自动进行分配和释放,栈区中 ...

最新文章

  1. RAP Mock.js语法规范
  2. java自学 day1
  3. Android 防止快速点击
  4. .Net开源 Shuttle(飞梭)服务总线(ESB)入门
  5. Apache部署网页-Ubuntu16.04
  6. 每周分享五个 PyCharm 使用技巧(四)
  7. yum安装ruby_centos 6.5 ruby环境安装
  8. HR怼程序员频繁跳槽,程序员竟这么回怼
  9. 智能会议系统---(4)VOIP 实现
  10. java心得体会2000字_java的学习心得体会
  11. android 手机 打印 图片,Mopria打印PDF、TXT文档或图片(适用于Android安卓系统)
  12. 荣耀9.0系统怎么无需root激活XPOSED框架的教程
  13. java高级实训输出张三李四_假设某数据库表中有一个姓名字段,查找姓名为张三和李四的条件是...
  14. python数据处理-整理表格数据
  15. qq三国行脚商脚本思路分享
  16. Java入门,最全面最简单的Java基础教程
  17. 网页中打开word文档
  18. python对mp3音乐剪切
  19. 西游记中孙悟空的家业历史
  20. 中小型企业如何进行网络安全防护?

热门文章

  1. ORACLE11g自动创建分区
  2. linux - android安卓 -dalvik
  3. ajax的三种方法以及ajax概念
  4. idea-设置代码块颜色
  5. ESP32设备驱动-BMA250加速度传感器驱动
  6. PHP实现即时消息通讯
  7. WebStorm 如何设置工具字体大小
  8. IGMP版本原理及比较
  9. java下标越界的三种处理方式,数组下标越界,该怎么解决
  10. Round Dance