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

本章写着写着就跑题了, 又不舍得删除, 新手看 # 协程的共享变量安全问题简单入门## 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 = 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
}
复制代码

可以看的出来还是存在线程安全问题, 而且协程的线程安全问题还更加不可预知, 用多线程的话, 我们都知道, 它一定线程不安全, 但使用的协程, 无法判断到底是不是同一个线程, 这时候就需要主动的打印出来到底是哪个线程需要上锁

此时没办法, 我们就可以去协程库里找找, 有没有那种专属的锁

image.png

发现还真有一个锁, 用用看看

@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给其他核心) 3. 核心B 缓存中没有, 发出 remote read 操作缓存中找到 b = 1, 执行 while 判断, 不满足跳出循环 4. 核心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循环 6. 核心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));

inline void oopDesc::release_int_field_put(int offset, jint contents) 
{
    OrderAccess::release_store(int_field_addr(offset), contents);
}
复制代码
inline void OrderAccess::release_store(volatile jint* p, jint v) { 
    release();
    *p = v; 
}
复制代码

storestore --> release

inline void OrderAccess::release() {
  WRITE_MEM_BARRIER;
}
复制代码
#define WRITE_MEM_BARRIER __asm __volatile ("":::"memory")
复制代码

这里需要了解下 gcc 的指令, 需要点别的知识, 我也不太了解, 知道他是内存屏障就行了, 具体可以百度 gcc内联汇编 + 你想要查询的关键字

上面这是写入的内存屏障, 而 如果是 volatile 变量的话, 在赋值结束之后还会调用:

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 另一个是 OrderAccess::storeload

同时我们发现了很多内存屏障

java 是个内存屏障

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

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 不是还 保证原子性 么?

相比很多人第一时间想到的是这样一段代码 :

@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步骤:

  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 的源码把这三个值找出来

image.png

这里设置了值, 这里的 value 被修饰成 volatile , 所以是 实际值

image.png

现在我们找旧值

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 版本果然

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 内部

image.png

我们找 window x86 文件

会发现有两个相同函数签名的 cmpxchg , 别急一个是 AMD 的, 我们找 intel 的,

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 有关系么??? !!!

猜你喜欢

转载自juejin.im/post/7036055747569909767