面试扫盲系列之:可见性与有序性的原理

面试扫盲系列之:可见性与有序性的原理,第1张

面试扫盲系列之:可见性与有序性的原理

参考《java高并发核心编程(卷2)》的章节编排,依次查阅资料总结(其实主要还是搞懂MM)

CPU物理缓存结构

L1高速缓存容量很小,但存取速度最快,并且紧靠着使用它的CPU内核。L2容量大一些,存取速度也慢一些,并且仍然只能被一个单独的CPU核使用。L3在现代多核CPU中更普遍,容量更大、读取速度更慢些,能被同一个CPU芯片板上的所有CPU内核共享。最后,系统还拥有一块主存(即主内存),由系统中的所有CPU共享。拥有L3高速缓存的CPU,CPU存取数据的命中率可达95%,也就是说只有不到5%的数据需要从主存中去存取。图片来源:JVM调优实战:五、CPU的内存结构以及JMM_一只不懈努力的程序猿,通过代码实验洞悉技术的本质。-CSDN博客_jvm的cpu

CPU 的频率太快了,使用缓存是为了提高效率。

MESI缓存一致性协议是解决加入缓存之后,带来的数据一致性问题。

MESI缓存一致性协议

既然每个核中都有单独的缓存,那我的 4 核 8 线程 CPU 处理主内存数据的时候,不就会出现数据不一致问题了吗?

为了解决这个问题,先后有过两种方法:总线锁机制和缓存锁机制。

总线锁就是使用 CPU 提供的一个LOCK#信号,当一个处理器在总线上输出此信号,其他处理器的请求将被阻塞,那么该处理器就可以独占共享锁。这样就保证了数据一致性。

但是总线锁开销太大,我们需要控制锁的粒度,所以又有了缓存锁,核心就是“缓存一致性协议”,不同的 CPU 硬件厂商实现方式稍有不同,有MSI、MESI、MOSI等。详细介绍:

https://weread.qq.com/web/reader/9b93254072456ac19b9a176k9fc324302859fc3d71522e0

JMM

JMM是一种规范,目的是解决由于多线程通过共享内存进行通信时,存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会对代码乱序执行等带来的问题。

面试官:既然CPU有MESI,为什么 JMM 还需要volatile关键字? - 掘金

有8大指令和8大规则:JVM调优实战:五、CPU的内存结构以及JMM_一只不懈努力的程序猿,通过代码实验洞悉技术的本质。-CSDN博客_jvm的cpu

JMM如何解决顺序一致性问题?

JMM提供了自己的内存屏障指令,要求JVM编译器实现这些指令,禁止特定类型的编译器和CPU重排序。由于不同CPU硬件实现内存屏障的方式不同,JMM屏蔽了这种底层CPU硬件平台的差异,定义了不对应任何CPU的JMM逻辑层内存屏障,由JVM在不同的硬件平台生成对应的内存屏障机器码。

JMM内存屏障主要有Load和Store两类,具体如下:(1)Load Barrier(读屏障)在读指令前插入读屏障,可以让高速缓存中的数据失效,重新从主存加载数据。(2)Store Barrier(写屏障)在写指令之后插入写屏障,能让写入缓存的最新数据写回主存。

volatile语义中的内存屏障

volatile语义中的有序性是通过内存屏障指令来确保的。

·在每个volatile读 *** 作的后面插入一个LoadLoad屏障。·在每个volatile读 *** 作的后面插入一个LoadStore屏障。·在每个volatile写 *** 作的前面插入一个StoreStore屏障。·在每个volatile写 *** 作的后面插入一个StoreLoad屏障。

happens-before

happens-before规则非常重要,它是判断数据是否存在竞争、线程是否安全的主要依据。,如果一个 *** 作执行的结果需要对另一个 *** 作可见,那么这两个 *** 作之间必须存在happens-before关系。【从JDK 5 开始,JMM就使用happens-before的概念来阐述多线程之间的内存可见性。】

happens-hefore不能理解为“时间上的先后顺序”   happens-before规则解析 - 知乎

Java高并发核心编程(卷2):多线程、锁、JMM、JUC、高并发设计模式-尼恩编著-微信读书

上面的链接有举例说明编程时怎么用happens-before规则。

volatile变量的复合 *** 作不具备原子性的原理

虽然volatile修饰的变量可以强制刷新内存,但是其并不具备原子性。虽然其要求对变量的(read、load、use)、(assign、store、write)必须是连续出现,但是在不同CPU内核上并发执行的线程还是有可能出现读取脏数据的时候。

假设有两个线程A、B分别运行在Core1、Core2上,并假设此时的value为0,线程A、B也都读取了value值到自己的工作内存。现在线程A将value变成1之后,完成了assign、store的 *** 作,假设在执行write指令之前,线程A的CPU时间片用完,线程A被空闲,但是线程A的write *** 作没有到达主存。由于线程A的store指令触发了写的信号,线程B缓存过期,重新从主存读取到value值,但是线程A的写入没有最终完成,线程B读到的value值还是0。线程B执行完成所有的 *** 作之后,将value变成1写入主存。线程A的时间片重新拿到,重新执行store *** 作,将过期了的1写入主存。

对于复合 *** 作,volatile变量无法保障其原子性,如果要保证复合 *** 作的原子性,就需要使用锁。并且,在高并发场景下,volatile变量一定需要使用Java的显式锁结合使用。

伪共享

例子:伪共享(false sharing),并发编程无声的性能杀手 - cyfonly - 博客园

有多个线程 *** 作不同的成员变量,但是在相同的缓存行时。

在一定线程数量范围内(注意思考:为什么强调是一定线程数量范围内),随着线程数量的增加,伪共享发生的频率也越大,直观体现就是执行时间越长。

一条缓存行有 64 字节,而 Java 程序的对象头固定占 8 字节(32位系统)或 12 字节( 64 位系统默认开启压缩, 不开压缩为 16 字节),所以我们只需要填 6 个无用的长整型补上6*8=48字节,让不同的 对象处于不同的缓存行,就避免了伪共享。

并不是每个系统都适合花大量精力去解决潜在的伪共享问题。
 

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zaji/5718236.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-18
下一篇 2022-12-17

发表评论

登录后才能评论

评论列表(0条)

保存