乐趣区

关于java:全网最硬核-Java-新内存模型解析与实验-3-硬核理解内存屏障CPU编译器

集体创作公约:自己申明创作的所有文章皆为本人原创,如果有参考任何文章的中央,会标注进去,如果有疏漏,欢送大家批评。如果大家发现网上有剽窃本文章的,欢送举报,并且踊跃向这个 github 仓库 提交 issue,谢谢反对~

本篇文章参考了大量文章,文档以及论文,然而这块货色真的很繁冗,我的程度无限,可能了解的也不到位,如有异议欢送留言提出。本系列会不断更新,联合大家的问题以及这里的谬误和疏漏,欢送大家留言

如果你喜爱单篇版,请拜访:全网最硬核 Java 新内存模型解析与试验单篇版(不断更新 QA 中)
如果你喜爱这个拆分的版本,这里是目录:

  • 全网最硬核 Java 新内存模型解析与试验 – 1. 什么是 Java 内存模型
  • 全网最硬核 Java 新内存模型解析与试验 – 2. 原子拜访与字决裂
  • 全网最硬核 Java 新内存模型解析与试验 – 3. 硬核了解内存屏障(CPU+ 编译器)
  • 全网最硬核 Java 新内存模型解析与试验 – 4. Java 新内存拜访形式与试验
  • 全网最硬核 Java 新内存模型解析与试验 – 5. JVM 底层内存屏障源码剖析
    JMM 相干文档:
  • Java Language Specification Chapter 17
  • The JSR-133 Cookbook for Compiler Writers – Doug Lea’s
  • Using JDK 9 Memory Order Modes – Doug Lea’s

内存屏障,CPU 与内存模型相干:

  • Weak vs. Strong Memory Models
  • Memory Barriers: a Hardware View for Software Hackers
  • A Detailed Analysis of Contemporary ARM and x86 Architectures
  • Memory Model = Instruction Reordering + Store Atomicity
  • Out-of-Order Execution

x86 CPU 相干材料:

  • x86 wiki
  • Intel® 64 and IA-32 Architectures Software Developer Manuals
  • Formal Specification of the x86 Instruction Set Architecture

ARM CPU 相干材料:

  • ARM wiki
  • aarch64 Cortex-A710 Specification

各种一致性的了解:

  • Coherence and Consistency

Aleskey 大神的 JMM 解说:

  • Aleksey Shipilëv – 不要误会 Java 内存模型(上)
  • Aleksey Shipilëv – 不要误会 Java 内存模型(下)

置信很多 Java 开发,都应用了 Java 的各种并发同步机制,例如 volatile,synchronized 以及 Lock 等等。也有很多人读过 JSR 第十七章 Threads and Locks(地址:https://docs.oracle.com/javase/specs/jls/se17/html/jls-17.html),其中包含同步、Wait/Notify、Sleep & Yield 以及内存模型等等做了很多标准解说。然而也置信大多数人和我一样,第一次读的时候,感觉就是在看热闹,看完了只是晓得他是这么规定的,然而为啥要这么规定,不这么规定会怎么样,并没有很清晰的意识。同时,联合 Hotspot 的实现,以及针对 Hotspot 的源码的解读,咱们甚至还会发现,因为 javac 的动态代码编译优化以及 C1、C2 的 JIT 编译优化,导致最初代码的体现与咱们的从标准上了解出代码可能的体现是不太统一的。并且,这种不统一,导致咱们在学习 Java 内存模型(JMM,Java Memory Model),了解 Java 内存模型设计的时候,如果想通过理论的代码去试,后果是与本人原本可能正确的了解被带偏了,导致误会。
我自己也是一直地尝试了解 Java 内存模型,重读 JLS 以及各路大神的剖析。这个系列,会梳理我集体在浏览这些标准以及剖析还有通过 jcstress 做的一些试验而得出的一些了解,心愿对于大家对 Java 9 之后的 Java 内存模型以及 API 形象的了解有所帮忙。然而,还是强调一点,内存模型的设计,出发点是让大家能够不必关怀底层而形象进去的一些设计,波及的货色很多,我的程度无限,可能了解的也不到位,我会尽量把每一个论点的论据以及参考都摆出来,请大家不要齐全置信这里的所有观点,如果有任何异议欢送带着具体的实例反驳并留言

5. 内存屏障

5.1. 为何须要内存屏障

内存屏障(Memory Barrier),也有叫内存栅栏(Memory Fence),还有的材料间接为了简便,就叫 membar,这些其实意思是一样的。内存屏障次要为了解决指令乱序带来了后果与预期不统一的问题,通过退出内存屏障避免指令乱序(或者称为重排序,reordering)。

那么为什么会有指令乱序呢?次要是因为 CPU 乱序(CPU 乱序还包含 CPU 内存乱序以及 CPU 指令乱序)以及编译器乱序。内存屏障能够用于避免这些乱序。如果内存屏障对于编译器和 CPU 都失效,那么个别称为硬件内存屏障,如果只对编译器失效,那么个别被称为软件内存屏障。咱们这里次要关注 CPU 带来的乱序,对于编译器的重排序咱们会在最初简要介绍下。

5.2. CPU 内存乱序相干

咱们从 CPU 高速缓存以及缓存一致性协定登程,开始剖析为何 CPU 中会有乱序。咱们这里假如 一种繁难的 CPU 模型 请大家肯定记住,理论的 CPU 要比这里列举的繁难 CPU 模型简单的多

5.2.1. 繁难 CPU 模型 – CPU 高速缓存的出发点 – 缩小 CPU Stall

咱们在这里会看到,古代的 CPU 的很多设计,所有以缩小 CPU Stall 登程。什么是 CPU Stall 呢?举一个简略的例子,假如 CPU 须要间接读取内存中的数据(疏忽其余的构造,例如 CPU 缓存,总线与总线事件等等):

CPU 收回读取申请,在内存响应之前,CPU 须要始终期待,无奈解决其余的事件。这一段 CPU 就是 处于 Stall 状态 。如果 CPU 始终间接从内存中读取,CPU 间接拜访内存耗费工夫很长,可能须要 几百个指令周期 ,也就是每次拜访都会有几百个指令周期内 CPU 处于 Stall 状态什么也干不了,这样效率会很低。个别须要引入 若干个高速缓存(Cache)来缩小 Stall:高速缓存即与处理器紧挨着的小型存储器,位于处理器和内存之间。

咱们这里 不关怀多级高速缓存,以及是否存在多个 CPU 共用某一缓存的状况 ,咱们就简略认为是上面这个架构:

当须要读取一个地址的值时,拜访高速缓存看是否存在:存在代表 命中 (hit),间接读取。不存在被称为 缺失 (miss)。同样的,如果须要写一个值到一个地址,这个地址在缓存中存在也就不须要拜访内存了。大部分程序都体现出较高的 局部性(locality):

  • 如果处理器读或写一个内存地址,那么它很可能很快还会读或写同一个地址
  • 如果处理器读或写一个内存地址,那么它很可能很快还会读或写左近的地址

针对局部性,高速缓存个别会一次操作不止一个字,而是 一组邻近的字 ,称为 缓存行

然而呢,因为通知缓存的存在,就给更新内存带来了麻烦:当一个 CPU 须要更新一块缓存行对应内存的时候,它须要将其余 CPU 缓存中这块内存的缓存行也置为生效。为了维持每个 CPU 的缓存数据一致性,引入了缓存一致性协定(Cache Coherence Protocols)

5.2.2. 繁难 CPU 模型 – 一种简略的缓存一致性协定(理论的 CPU 用的要比这个简单) – MESI

古代的缓存一致性的协定以及算法非常复杂,缓存行可能会有数十种不同的状态。这里咱们并不需要钻研这种简单的算法,咱们这里引入一个最经典最简略的缓存一致性协定即 4 状态 MESI 协定(再次强调,理论的 CPU 用的协定要比这个简单,MESI 其实自身有些问题解决不了),MESI 其实指的就是缓存行的四个状态:

  • Modified:缓存行被批改,最终肯定会被写回入主存,在此之前其余处理器不能再缓存这个缓存行。
  • Exclusive:缓存行还未被批改,然而其余的处理器不能将这个缓存行载入缓存
  • Shared:缓存行未被批改,其余处理器能够加载这个缓存行到缓存
  • Invalid:缓存行中没有有意义的数据

依据咱们后面的 CPU 缓存结构图中所示,假如所有 CPU 都共用在同一个总线上,则会有如下这些信息在总线上发送:

  1. Read:这个事件蕴含要读取的缓存行的物理地址。
  2. Read Response:蕴含后面的读取事件申请的数据,数据起源可能是 内存或者是其余高速缓存,例如,如果申请的数据在其余缓存处于 modified 状态的话,那么必须从这个缓存读取缓存行数据作为 Read Response
  3. Invalidate:这个事件蕴含要过期掉的缓存行的物理地址。其余的高速缓存必须移除这个缓存行并且响应 Invalidate Acknowledge 音讯。
  4. Invalidate Acknowledge:收到 Invalidate 音讯移除掉对应的缓存行之后,回复 Invalidate Acknowledge 音讯。
  5. Read Invalidate:是 Read 音讯还有 Invalidate 音讯的组合,蕴含要读取的缓存行的物理地址。既读取这个缓存行并且须要 Read Response 音讯响应,同时发给其余的高速缓存,移除这个缓存行并且响应 Invalidate Acknowledge 音讯。
  6. Writeback:这个音讯蕴含要更新的内存地址以及数据。同时,这个音讯也容许状态为 modified 的缓存行被剔除,以给其余数据腾出空间。

缓存行状态转移与事件 的关系:

这里只是列出这个图,咱们不会深刻去讲的,因为 MESI 是一个十分精简的协定,具体实现的时候会有很多额定的问题 MESI 无奈解决 ,如果具体的去讲,会把读者绕进去,读者会思考在某个极限状况下这个协定要怎么做能力保障正确,然而 MESI 实际上解决不了这些。在 理论的实现中,CPU 一致性协定要比 MESI 简单的多得多,然而个别都是基于 MESI 扩大的

举一个简略的 MESI 的例子:

1.CPU A 发送 Read 从地址 a 读取数据,收到 Read Response 将数据存入他的高速缓存并将对应的缓存行置为 Exclusive

2.CPU B 发送 Read 从地址 a 读取数据,CPU A 检测到地址抵触,CPU A 响应 Read Response 返回缓存中蕴含 a 地址的缓存行数据,之后,地址 a 的数据对应的缓存行被 A 和 B 以 Shared 状态装入缓存


3.CPU B 对于 a 马上要进行写操作,发送 Invalidate,期待 CPU A 的 Invalidate Acknowledge 响应之后,状态批改为 Exclusive。CPU A 收到 Invalidate 之后,将 a 所在的缓存行状态置为 Invalid 生效

4.CPU B 批改数据存储到蕴含地址 a 的缓存行上,缓存行状态置为 modified

5. 这时候 CPU A 又须要 a 数据,发送 Read 从地址 a 读取数据,CPU B 检测到地址抵触,CPU B 响应 Read Response 返回缓存中蕴含 a 地址的缓存行数据,之后,地址 a 的数据对应的缓存行被 A 和 B 以 Shared 状态装入缓存

咱们这里能够看到,MESI 协定中,发送 Invalidate 音讯须要以后 CPU 期待其余 CPU 的 Invalidate Acknowledge,也就是 这里有 CPU Stall。为了防止这个 Stall,引入了 Store Buffer

5.2.3. 繁难 CPU 模型 – 防止期待 Invalidate Response 的 Stall – Store Buffer

为了防止这种 Stall,在 CPU 与 CPU 缓存之间增加 Store Buffer,如下图所示:

有了 Store Buffer,CPU 在发送 Invalidate 音讯的时候,不必期待 Invalidate Acknowledge 的返回,将批改的数据间接放入 Store Buffer。如果收到了所有的 Invalidate Acknowledge 再从 Store Buffer 放入 CPU 的高速缓存的对应缓存行中。然而退出的这个 Store Buffer 又带来了新的问题:

假如有两个变量 a 和 b,不会处于同一个缓存行,初始都是 0,a 当初位于 CPU A 的缓存行中,b 当初位于 CPU B 的缓存行中:

假如 CPU B 要执行上面的代码:


咱们必定是冀望最初 b 会等于 2 的。然而真的会如咱们所愿么?咱们来具体看下上面这个运行步骤:

1.CPU B 执行 a = 1:

(1)因为 CPU B 缓存中没有 a,并且要批改,所以公布 Read Invalidate 音讯(因为是要先把蕴含 a 的整个缓存行读取后能力更新,所以发的是 Read Invalidate,而不只是 Invalidate)。

(2)CPU B 将 a 的批改(a=1)放入 Storage Buffer

(3)CPU A 收到 Read Invalidate 音讯,将 a 所在的缓存行标记为 Invalid 并革除出缓存,并响应 Read Response(a=0)和 Invalidate Acknowlegde


2.CPU B 执行 b = a + 1:

(1)CPU B 收到来自于 CPU A 的 Read Response,这时候这外面 a 还是等于 0。

(2)CPU B 将 a + 1 的后果 (0+1=1) 存入缓存中曾经蕴含的 b。

3.CPU B 执行 assert(b == 2) 失败

这个谬误的起因次要是咱们在加载到缓存的时候没思考从 store buffer 最新的值,所以咱们能够加上一步,在加载到缓存的时候从 store buffer 读取最新的值。这样,就能保障下面咱们看到的后果 b 最初是 2:

5.2.4. 繁难 CPU 模型 – 防止 Store Buffer 带来的乱序执行 – 内存屏障

咱们上面再来看一个示例:假如有两个变量 a 和 b,不会处于同一个缓存行,初始都是 0。假如 CPU A(缓存行外面蕴含 b,这个缓存行状态是 Exclusive)执行:

假如 CPU B 执行:

如果所有依照程序程序预期执行,那么咱们冀望 CPU B 执行 assert(a == 1) 是胜利的,然而咱们来看上面这种执行流程:

1.CPU A 执行 a = 1:

(1)CPU A 缓存外面没有 a,并且要批改,所以公布 Read Invalidate 音讯。

(2)CPU A 将 a 的批改(a=1)放入 Storage Buffer

2.CPU B 执行 while (b == 0) continue:

(1)CPU B 缓存外面没有 b,公布 Read 音讯。

3.CPU A 执行 b = 1:

(1)CPU A 缓存行外面有 b,并且状态是 Exclusive,间接更新缓存行。

(2)之后,CPU A 收到了来自于 CPU B 的对于 b 的 Read 音讯。

(3)CPU A 响应缓存中的 b = 1,发送 Read Response 音讯,并且缓存行状态批改为 Shared

(4)CPU B 收到 Read Response 音讯,将 b 放入缓存

(5)CPU B 代码能够退出循环了,因为 CPU B 看到 b 此时为 1

4.CPU B 执行 assert(a == 1),然而因为 a 的更改还没更新,所以失败了。

像这种乱序,CPU 个别是无奈自动控制的,然而个别会提供内存屏障指令,通知 CPU 避免乱序,例如:


smp_mb() 会让 CPU 将 Store Buffer 中的内容刷入缓存。退出这个内存屏障指令后,执行流程变成:


1.CPU A 执行 a = 1:

(1)CPU A 缓存外面没有 a,并且要批改,所以公布 Read Invalidate 音讯。

(2)CPU A 将 a 的批改(a=1)放入 Storage Buffer

2.CPU B 执行 while (b == 0) continue:

(1)CPU B 缓存外面没有 b,公布 Read 音讯。

3.CPU B 执行 smp_mb():

(1)CPU B 将以后 Store Buffer 的所有条目打上标记(目前这里只有 a,就是对 a 打上标记)

4.CPU A 执行 b = 1:

(1)CPU A 缓存行外面有 b,并且状态是 Exclusive,然而因为 Store Buffer 中有标记的条目 a,不间接更新缓存行,而是放入 Store Buffer(与 a 不同,没有标记)。并收回 Invalidate 音讯。

(2)之后,CPU A 收到了来自于 CPU B 的对于 b 的 Read 音讯。

(3)CPU A 响应缓存中的 b = 0,发送 Read Response 音讯,并且缓存行状态批改为 Shared

(4)CPU B 收到 Read Response 音讯,将 b 放入缓存

(5)CPU B 代码一直循环,因为 CPU B 看到 b 还是 0

(6)CPU A 收到后面对于 a 的 “Read Invalidate” 相干的音讯响应,将 Store Buffer 中打好标记的 a 条目刷入缓存,这个缓存行状态为 modified。

(7)CPU B 收到 CPU A 发的 Invalidate b 的音讯,将 b 的缓存行生效,回复 Invalidate Acknowledge

(8)CPU A 收到 Invalidate Acknowledge,将 b 从 Store Buffer 刷入缓存。

(9)因为 CPU B 一直读取 b,然而 b 曾经不在缓存中了,所以发送 Read 音讯。

(10)CPU A 收到 CPU B 的 Read 音讯,设置 b 的缓存行状态为 shared,返回缓存中 b = 1 的 Read Response

(11)CPU B 收到 Read Response,得悉 b = 1,放入缓存行,状态为 shared

5.CPU B 得悉 b = 1,退出 while (b == 0) continue 循环

6.CPU B 执行 assert(a == 1)(这个比较简单,就不画图了):
(1)CPU B 缓存中没有 a,收回 Read 音讯。
(2)CPU A 从缓存中读取 a = 1,响应 Read Response
(3)CPU B 执行 assert(a == 1) 胜利

Store Buffer 个别都会比拟小,如果 Store Buffer 满了,那么还是会产生 Stall 的问题。咱们冀望 Store Buffer 能比拟快的刷入 CPU 缓存,这是在收到对应的 Invalidate Acknowledge 之后进行的。然而,其余的 CPU 可能在忙,没发很快应答收到的 Invalidate 音讯并响应 Invalidate Acknowledge,这样可能造成 Store Buffer 满了导致 CPU Stall 的产生。所以,能够引入每个 CPU 的 Invalidate queue 来缓存要解决的 Invalidate 音讯。

5.2.5. 繁难 CPU 模型 – 解耦 CPU 的 Invalidate 与 Store Buffer – Invalidate Queues

退出 Invalidate Queues 之后,CPU 构造如下所示:

有了 Invalidate Queue,CPU 能够将 Invalidate 放入这个队列之后立即将 Store Buffer 中的对应数据刷入 CPU 缓存。同时,CPU 在想被动发某个缓存行的 Invalidate 音讯之前,必须查看本人的 Invalidate Queue 中是否有雷同的缓存行的 Invalidate 音讯。如果有,必须等解决完本人的 Invalidate Queue 中的对应音讯再发。

同样的,Invalidate Queue 也带来了乱序执行。

5.2.6. 繁难 CPU 模型 – 因为 Invalidate Queues 带来的进一步乱序 – 须要内存屏障

假如有两个变量 a 和 b,不会处于同一个缓存行,初始都是 0。假如 CPU A(缓存行外面蕴含 a(shared), b(Exclusive))执行:


CPU B(缓存行外面蕴含 a(shared))执行:


1.CPU A 执行 a = 1:

(1)CPU A 缓存外面有 a(shared),CPU A 将 a 的批改(a=1)放入 Store Buffer,发送 Invalidate 音讯。

2.CPU B 执行 while (b == 0) continue:

(1)CPU B 缓存外面没有 b,公布 Read 音讯。

(2)CPU B 收到 CPU A 的 Invalidate 音讯,放入 Invalidate Queue 之后立即返回。

(3)CPU A 收到 Invalidate 音讯的响应,将 Store Buffer 中的缓存行刷入 CPU 缓存

3.CPU A 执行 smp_mb():

(1)因为 CPU A 曾经把 Store Buffer 中的缓存行刷入 CPU 缓存,所以这里间接通过


4.CPU A 执行 b = 1:

(1)因为 CPU A 自身蕴含 b 的缓存行 (Exclusive),间接更新缓存行即可。

(2)CPU A 收到 CPU B 之前发的 Read 音讯,将 b 的缓存行状态更新为 Shared,之后发送 Read Response 蕴含 b 的最新值

(3)CPU B 收到 Read Response,b 的值为 1

5.CPU B 退出循环,开始执行 assert(a == 1)

(1)因为目前对于 a 的 Invalidate 音讯还在 Invalidate queue 中没有解决,所以 CPU B 看到的还是 a = 0,assert 失败

所以,咱们针对这种乱序,在 CPU B 执行的代码中也退出内存屏障,这里内存屏障不仅期待 CPU 刷完所有的 Store Buffer,还要期待 CPU 的 Invalidate Queue 全副解决完。退出内存屏障,CPU B 执行的代码是:

这样,在后面的第 5 步,CPU B 退出循环,执行 assert(a == 1) 之前须要期待 Invalidate queue 解决完:
(1)解决 Invalidate 音讯,将 b 置为 Invalid
(2)持续代码,执行 assert(a == 1),这时候缓存内不存在 b,须要发 Read 音讯,这样就能看到 b 的最新值 1 了,assert 胜利。

5.2.7. 繁难 CPU 模型 – 更细粒度的内存屏障

咱们后面提到,在咱们后面提到的 CPU 模型中,smp_mb() 这个内存屏障指令,做了两件事:期待 CPU 刷完所有的 Store Buffer,期待 CPU 的 Invalidate Queue 全副解决完。然而,对于咱们这里 CPU A 与 CPU B 执行的代码中的内存屏障,并不是每次都要这两个操作同时存在:

所以,个别 CPU 还会形象出更细粒度的内存屏障指令,咱们这里管期待 CPU 刷完所有的 Store Buffer 的指令叫做 写内存屏障 (Write Memory Buffer),期待 CPU 的 Invalidate Queue 全副解决完的指令叫做 读内存屏障(Read Memory Buffer)。

5.2.8. 繁难 CPU 模型 – 总结

咱们这里通过一个简略的 CPU 架构登程,层层递进,讲述了一些繁难的 CPU 构造以及为何会须要内存屏障,能够总结为上面这个简略思路流程图:

  1. CPU 每次间接拜访内存太慢,会让 CPU 始终处于 Stall 期待。为了缩小 CPU Stall,退出了 CPU 缓存
  2. CPU 缓存带来了多 CPU 间的缓存不一致性,所以 通过 MESI 这种繁难的 CPU 缓存一致性协定协调不同 CPU 之间的缓存一致性
  3. 对于 MESI 协定中的一些机制进行优化,进一步缩小 CPU Stall:

    1. 通过将更新放入 Store Buffer,让更新收回的 Invalidate 音讯不必 CPU Stall 期待 Invalidate Response。
    2. Store Buffer 带来了指令 (代码) 乱序,须要内存屏障指令,强制以后 CPU Stall 期待刷完所有 Store Buffer 中的内容。这个内存屏障指令个别称为写屏障。
    3. 为了放慢 Store Buffer 刷入缓存,减少 Invalidate Queue,

5.3. CPU 指令乱序相干

CPU 指令的执行,也可能会乱序,咱们这里只说一种比拟常见的 – 指令并行化。

5.3.1. 减少 CPU 执行效率 – CPU 流水线模式(CPU Pipeline)

古代 CPU 在执行指令时,是以指令流水线的模式来运行的。因为 CPU 外部也有不同的组件,咱们能够将执行一条指令分成不同阶段,不同的阶段波及的组件不同,这样伪解耦能够让每个组件独立的执行,不必期待一个指令齐全执行完再解决下一个指令。

个别分为如下几个阶段:取指 (Instrcution Fetch,IF)、 译码 (Instruction Decode,ID)、 执行 (Execute,EXE)、 存取 (Memory,MEM)、 写回(Write-Back,WB)

5.3.2. 进一步升高 CPU Stall – CPU 乱序流水线(Out of order execution Pipeline)

因为指令的数据是否就绪也是不确定的,比方上面这个例子:

假使数据 a 没有就绪,还没有载入到寄存器,那么咱们其实没必要 Stall 期待加载 a,能够先执行 c = 1; 由此,咱们能够将程序中,能够并行的指令提取进去同时安顿执行,CPU 乱序流水线(Out of order execution Pipeline)就是基于这种思路:

如图所示,CPU 的执行阶段分为:

  1. Instructions Fetch:批量拉取一批指令,进行指令剖析,剖析其中的循环以及依赖,分支预测等等
  2. Instruction Decode:指令译码,与后面的流水线模式大同小异
  3. Reservation stations:须要操作数输出的指令,如果输出就绪,就进入 Functoinal Unit (FU) 解决,如果没有没有就绪就监听 Bypass network,数据就绪发回信号到 Reservation stations,让指令进图 FU 解决。
  4. Functional Unit:解决指令
  5. Reorder Buffer:会将指令依照原有程序的程序保留,这些指令会在被 dispatched 后增加到列表的一端,而当他们实现执行后,从列表的另一端移除。通过这种形式,指令会按他们 dispatch 的程序实现。

这样的结构设计下,能够保障写入 Store Buffer 的程序,与原始的指令程序一样。然而加载数据,以及计算,是并行执行的。后面咱们曾经晓得了在咱们的繁难 CPU 架构外面,有着多 CPU 缓存 MESI,Store Buffer 以及 Invalidate Queue 导致读取不到最新的值,这里的乱序并行加载以及解决更加剧了这一点。并且,结构设计下,仅能保障检测出同一个线程下的指令之间的相互依赖,保障这样的相互依赖之间的指令执行程序是对的,然而多线程程序之间的指令依赖,CPU 批量取指令以及分支预测是无奈感知的。所以还是会有乱序。这种乱序,同样能够通过后面的内存屏障防止

5.4. 理论的 CPU

理论的 CPU 多种多样,有着不同的 CPU 结构设计以及不同的 CPU 缓存一致性协定,就会有 不同品种的乱序,如果每种独自来看,就太简单了。所以,大家通过一种规范来形象形容不同的 CPU 的乱序景象(即第一个操作为 M,第二个操作为 N,这两个操作是否会乱序,是不是很像 Doug Lea 对于 JMM 的形容,其实 Java 内存模型也是参考这个设计的),参考上面这个表格:

咱们先来说一下每一列的意思:

  1. Loads Reordered After Loads:第一个操作是读取,第二个也是读取,是否会乱序。
  2. Loads Reordered After Stores:第一个操作是读取,第二个是写入,是否会乱序。
  3. Stores Reordered After Stores:第一个操作是写入,第二个也是写入,是否会乱序。
  4. Stores Reordered After Loads:第一个操作是写入,第二个是读取,是否会乱序。
  5. Atomic Instructions Reordered With Loads:两个操作是原子操作(一组操作,同时产生,例如同时批改两个字这种指令)与读取,这两个相互是否会乱序。
  6. Atomic Instructions Reordered With Stores:两个操作是原子操作(一组操作,同时产生,例如同时批改两个字这种指令)与写入,这两个相互是否会乱序。
  7. Dependent Loads Reordered:如果一个读取依赖另一个读取的后果,是否会乱序。
  8. Incoherent Instruction Cache/Pipeline:是否会有指令乱序执行。

举一个例子来看即咱们本人的 PC 下面罕用的 x86 构造,在这种构造下,仅仅会产生 Stores Reordered After Loads 以及 Incoherent Instruction Cache/Pipeline。其实前面要提到的 LoadLoad,LoadStore,StoreLoad,StoreStore 这四个 Java 中的内存屏障,为啥在 x86 的环境下其实只须要实现 StoreLoad,其实就是这个起因。

5.5. 编译器乱序

除了 CPU 乱序以外,在软件层面还有编译器优化重排序导致的,其实编译器优化的一些思路与下面说的 CPU 的指令流水线优化其实有些相似。比方编译器也会剖析你的代码,对互相不依赖的语句进行优化。对于互相没有依赖的语句,就能够随便的进行重排了。然而同样的,编译器也是只能从单线程的角度去思考以及剖析,并不知道你程序在多线程环境下的依赖以及分割。再举一个简略的例子,假如没有任何 CPU 乱序的环境下,有两个变量 x = 0,y = 0,线程 1 执行:

线程 2 执行:

那么线程 2 是可能 assert 失败的,因为编译器可能会让 x = 1y = 1 之间乱序。

编译器乱序,能够通过减少不同操作系统上的编译器屏障语句进行防止。例如线程一执行:

这样就不会呈现 x = 1y = 1 之间乱序的状况。

同时,咱们在理论应用的时候,个别内存屏障指的是硬件内存屏障,即通过硬件 CPU 指令实现的内存屏障,这种硬件内存屏障个别也会隐式地带上编译器屏障。编译器屏障个别被称为软件内存屏障,仅仅是管制编译器软件层面的屏障,举一个例子即 C++ 中的 volaile,它与 Java 中的 volatile 不一样,C++ 中的 volatile 仅仅是禁止编译器重排即有编译器屏障,然而无奈防止 CPU 乱序。

以上,咱们就根本搞清楚了乱序的起源,以及内存屏障的作用。接下来,咱们行将步入正题,开始咱们的 Java 9+ 内存模型之旅。在这之前,再说一件须要留神的事件:为什么最好不要本人写代码验证 JMM 的一些论断,而是应用业余的框架去测试

微信搜寻“我的编程喵”关注公众号,加作者微信,每日一刷,轻松晋升技术,斩获各种 offer

我会常常发一些很好的各种框架的官网社区的新闻视频材料并加上集体翻译字幕到如下地址(也包含下面的公众号),欢送关注:

  • 知乎:https://www.zhihu.com/people/…
  • B 站:https://space.bilibili.com/31…
退出移动版