2727
2828我们以多核 CPU 为例,每个CPU 核都包含** 一组 「CPU 寄存器」** ,这些寄存器本质上是在 CPU 内存中。CPU 在这些寄存器上执行操作的速度要比在主内存(RAM)中执行的速度快得多。
2929
30- 因为** CPU速率高, 内存速率慢,为了让存储体系可以跟上CPU的速度 ,所以中间又加上 Cache 层,就是我们说的 「 CPU 高速缓存」 ** 。
30+ 因为** CPU速率高, 内存速率慢,为了让存储体系可以跟上 CPU 的速度 ,所以中间又加上 Cache 层,就是我们说的 『 CPU 高速缓存』 ** 。
3131
3232### CPU多级缓存
3333
34- 由于CPU的运算速度远远超越了1级缓存的数据I\O能力,CPU厂商又引入了多级的缓存结构。通常L1 、L2 是每个CPU 核有一个,L3 是多个核共用一个。
34+ 由于 CPU 的运算速度远远超越了 1 级缓存的数据 I\O 能力,CPU 厂商又引入了多级的缓存结构。通常 L1 、L2 是每个 CPU 核有一个,L3 是多个核共用一个。
3535
3636### Cache Line
3737
38- Cache又是由很多个 ** 「缓存行」** (Cache line) 组成的。Cache line 是 Cache 和 RAM 交换数据的最小单位。
38+ Cache 又是由很多个 ** 「缓存行」** (Cache line) 组成的。Cache line 是 Cache 和 RAM 交换数据的最小单位。
3939
40- Cache 存储数据是固定大小为单位的,称为一个** Cache entry** ,这个单位称为** Cache line** 或 ** Cache block** 。给定Cache 容量大小和 Cache line size 的情况下,它能存储的条目个数(number of cache entries)就是固定的。因为Cache 是固定大小的,所以它从主内存获取数据也是固定大小。对于X86来讲 ,是 64Bytes。对于ARM来讲,较旧的架构的Cache line是32Bytes ,但一次内存访存只访问一半的数据也不太合适,所以它经常是一次填两个 Cache line,叫做 double fill。
40+ Cache 存储数据是固定大小为单位的,称为一个** Cache entry** ,这个单位称为 ** Cache line** 或 ** Cache block** 。给定 Cache 容量大小和 Cache line size 的情况下,它能存储的条目个数(number of cache entries)就是固定的。因为Cache 是固定大小的,所以它从主内存获取数据也是固定大小。对于 X86 来讲 ,是 64Bytes。对于 ARM 来讲,较旧的架构的 Cache line 是 32Bytes ,但一次内存访存只访问一半的数据也不太合适,所以它经常是一次填两个 Cache line,叫做 double fill。
4141
4242
4343
@@ -65,13 +65,13 @@ Cache 存储数据是固定大小为单位的,称为一个**Cache entry**,
6565
6666总线锁就是使用 CPU 提供的一个` LOCK# ` 信号,当一个处理器在总线上输出此信号,其他处理器的请求将被阻塞,那么该处理器就可以独占共享锁。这样就保证了数据一致性。
6767
68- 但是总线锁开销太大,我们需要控制锁的粒度,所以又有了缓存锁,核心就是“** 缓存一致性协议** ”,不同的 CPU 硬件厂商实现方式稍有不同,有MSI 、MESI、MOSI等。
68+ 但是总线锁开销太大,我们需要控制锁的粒度,所以又有了缓存锁,核心就是“** 缓存一致性协议** ”,不同的 CPU 硬件厂商实现方式稍有不同,有 MSI 、MESI、MOSI等。
6969
7070
7171
7272### 代码乱序执行优化
7373
74- 为了使得处理器内部的运算单元尽量被充分利用,提高运算效率,处理器可能会对输入的代码进行「乱序执行」** (Out-Of-Order Execution),处理器会在计算之后将乱序执行的结果重组** ,乱序优化可以保证在单线程下该执行结果与顺序执行的结果是一致的,但不保证程序中各个语句计算的先后顺序与输入代码中的顺序一致。
74+ 为了使得处理器内部的运算单元尽量被充分利用,提高运算效率,处理器可能会对输入的代码进行「** 乱序执行** 」** (Out-Of-Order Execution),处理器会在计算之后将乱序执行的结果重组** ,乱序优化可以保证在单线程下该执行结果与顺序执行的结果是一致的,但不保证程序中各个语句计算的先后顺序与输入代码中的顺序一致。
7575
7676** 乱序执行技术是处理器为提高运算速度而做出违背代码原有顺序的优化** 。在单核时代,处理器保证做出的优化不会导致执行结果远离预期目标,但在多核环境下却并非如此。
7777
@@ -84,9 +84,7 @@ Cache 存储数据是固定大小为单位的,称为一个**Cache entry**,
8484
8585### 内存屏障
8686
87- 又称为内存栅栏,是一个 CPU 指令。尽管我们看到乱序执行初始目的是为了提高效率,但是它看来其好像在这多核时代不尽人意,其中的某些”自作聪明”的优化导致多线程程序产生各种各样的意外。因此有必要存在一种机制来消除乱序执行带来的坏影响,也就是说应该允许程序员显式的告诉处理器对某些地方禁止乱序执行。这种机制就是所谓内存屏障。不同架构的处理器在其指令集中提供了不同的指令来发起内存屏障,对应在编程语言当中就是提供特殊的关键字来调用处理器相关的指令,JMM里我们再探讨。
88-
89-
87+ 又称为内存栅栏,是一个 CPU 指令。尽管我们看到乱序执行初始目的是为了提高效率,但是它看来其好像在这多核时代不尽人意,其中的某些”自作聪明”的优化导致多线程程序产生各种各样的意外。因此有必要存在一种机制来消除乱序执行带来的坏影响,也就是说应该允许程序员显式的告诉处理器对某些地方禁止乱序执行。这种机制就是所谓内存屏障。不同架构的处理器在其指令集中提供了不同的指令来发起内存屏障,对应在编程语言当中就是提供特殊的关键字来调用处理器相关的指令,JMM 里我们再探讨。
9088
9189------
9290
@@ -148,9 +146,9 @@ JMM 与 Java 内存区域中的堆、栈、方法区等并不是同一个层次
148146
149147如果两个或多个线程共享一个对象,并且多个线程更新该共享对象中的变量,则可能出现竞争条件。
150148
151- 想象一下,如果线程A将一个共享对象的变量读入到它的CPU缓存中 。此时,线程B执行相同的操作,但是进入不同的CPU缓存 。现在线程A执行 +1 操作,线程B也这样做。现在该变量增加了两次,在每个CPU缓存中一次 。
149+ 想象一下,如果线程 A 将一个共享对象的变量读入到它的 CPU 缓存中 。此时,线程 B 执行相同的操作,但是进入不同的 CPU 缓存 。现在线程A执行 +1 操作,线程B也这样做。现在该变量增加了两次,在每个 CPU 缓存中一次 。
152150
153- 如果这些增量是按顺序执行的,则变量结果会是3 ,并将原始值+ 2写回主内存 。但是,这两个增量是同时执行的,没有适当的同步。不管将哪个线程的结构写回主内存,更新后的值只比原始值高1,显然是有问题的。如下(当然可以用 Java 提供的关键字 Synchronized)
151+ 如果这些增量是按顺序执行的,则变量结果会是 3 ,并将原始值 +2 写回主内存 。但是,这两个增量是同时执行的,没有适当的同步。不管将哪个线程的结构写回主内存,更新后的值只比原始值高1,显然是有问题的。如下(当然可以用 Java 提供的关键字 Synchronized)
154152
155153 ![ ] ( https://tva1.sinaimg.cn/large/00831rSTly1gcw2i23173j30pu0hqgml.jpg )
156154
@@ -218,15 +216,15 @@ Java 内存模型要求 lock,unlock,read,load,assign,use,store,wri
218216
219217先行发生(happens-before)是 Java 内存模型中定义的两项操作之间的偏序关系,** 如果操作A 先行发生于操作B,那么A的结果对B可见** 。happens-before关系的分析需要分为** 单线程和多线程** 的情况:
220218
221- - ** 单线程下的 happens-before** 字节码的先后顺序天然包含happens-before关系 :因为单线程内共享一份工作内存,不存在数据一致性的问题。 在程序控制流路径中靠前的字节码 happens-before 靠后的字节码,即靠前的字节码执行完之后操作结果对靠后的字节码可见。然而,这并不意味着前者一定在后者之前执行。实际上,如果后者不依赖前者的运行结果,那么它们可能会被重排序。
222- - ** 多线程下的 happens-before** 多线程由于每个线程有共享变量的副本,如果没有对共享变量做同步处理,线程1更新执行操作A共享变量的值之后,线程2开始执行操作B,此时操作A产生的结果对操作B不一定可见 。
219+ - ** 单线程下的 happens-before** 字节码的先后顺序天然包含 happens-before 关系 :因为单线程内共享一份工作内存,不存在数据一致性的问题。 在程序控制流路径中靠前的字节码 happens-before 靠后的字节码,即靠前的字节码执行完之后操作结果对靠后的字节码可见。然而,这并不意味着前者一定在后者之前执行。实际上,如果后者不依赖前者的运行结果,那么它们可能会被重排序。
220+ - ** 多线程下的 happens-before** 多线程由于每个线程有共享变量的副本,如果没有对共享变量做同步处理,线程 1 更新执行操作 A 共享变量的值之后,线程 2 开始执行操作 B,此时操作 A 产生的结果对操作 B 不一定可见 。
223221
224- 为了方便程序开发,Java 内存模型实现了下述的先行发生关系:
222+ 为了方便程序开发,Java 内存模型实现了下述的先行发生关系(“天然的”先行发生关系,无需任何同步器协助就存在) :
225223
226224- ** 程序次序规则:** 一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作。
227- - ** 管程锁定规则:** 一个unLock操作先行发生于后面对同一个锁的lock操作 。
228- - ** volatile变量规则:** 对一个变量的写操作 happens-before 后面对这个变量的读操作 。
229- - ** 传递规则:** 如果操作A 先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A 先行发生于操作C 。
225+ - ** 管程锁定规则:** 一个 unLock 操作先行发生于后面对同一个锁的 lock 操作 。
226+ - ** volatile变量规则:** 对一个变量的写操作先行发生于后面对这个变量的读操作 。
227+ - ** 传递规则:** 如果操作 A 先行发生于操作 B,而操作 B 又先行发生于操作 C,则可以得出操作 A 先行发生于操作 C 。
230228- ** 线程启动规则:** Thread对象的 ` start() ` 方法先行发生于此线程的每一个动作。
231229- ** 线程中断规则:** 对线程 ` interrupt() ` 方法的调用先行发生于被中断线程的代码检测到中断事件的发生。
232230- ** 线程终结规则:** 线程中所有的操作都先行发生于线程的终止检测,我们可以通过` Thread.join() ` 方法结束、` Thread.isAlive() ` 的返回值手段检测到线程已经终止执行。
@@ -252,7 +250,7 @@ Load2;
252250Load3;
253251```
254252
255- 对于上面的一组 CPU 指令(Store表示写入指令,Load表示读取指令),StoreLoad 屏障之前的 Store 指令无法与StoreLoad 屏障之后的 Load 指令进行交换位置,即** 重排序** 。但是 StoreLoad 屏障之前和之后的指令是可以互换位置的,即 Store1 可以和 Store2 互换,Load2 可以和 Load3 互换。
253+ 对于上面的一组 CPU 指令(Store表示写入指令,Load表示读取指令),StoreLoad 屏障之前的 Store 指令无法与 StoreLoad 屏障之后的 Load 指令进行交换位置,即** 重排序** 。但是 StoreLoad 屏障之前和之后的指令是可以互换位置的,即 Store1 可以和 Store2 互换,Load2 可以和 Load3 互换。
256254
257255常见的 4 种屏障
258256
0 commit comments