JMM与多核CPU

并行导致的问题

众所周知,我们写的进程跑在CPU上,进程下线程作为CPU调度的基本单位,而当今CPU基本都是多核CPU也就是说可以实现线程并行。

由于CPU计算极快,快到从主内存加载一次变量都要经过很多个时钟周期,为了提高CPU的利用率,在CPU内部引入了快速缓存。

那么线程跑的时候局部变量跑没有问题,但线程共享变量就会出现并发问题,因为他们拿到的都是从主内存中读到的共享变量的副本。

假设变量X = 0共享,两个线程同时执行X++

最后的结果可能就不是设计的2而是1。

因为X++需要先从主存中读出变量,再对变量进行更改,最后写回主存。

两个线程同时进行的话就会出现都加载变量X=0进入缓存内,各自将其加一后写回主内存。

最后的结果就是运行结束后X=1

解决

为了解决缓存变量不一致设计上有多种方案,比如总线锁与MESI协议。

总线锁就是在一个线程访问共享变量的时候其他CPU停止工作,使其独占变量。

MESI拆开英文是(Modified (修改状态)、Exclusive (独占状态)、Share(共享状态)、Invalid(无效状态)),他给变量设置有状态位。当每个CPU读取共享变量之前,会先识别数据的「对象状态」(是修改、还是共享、还是独占、还是无效)。

  • 如果是独占,说明当前CPU将要得到的变量数据是最新的,没有被其他CPU所同时读取
  • 如果是共享,说明当前CPU将要得到的变量数据还是最新的,有其他的CPU在同时读取,但还没被修改
  • 如果是修改,说明当前CPU正在修改该变量的值,同时会向其他CPU发送该数据状态为invalid(无效)的通知,得到其他CPU响应后(其他CPU将数据状态从共享(share)变成invalid(无效)),会当前CPU将高速缓存的数据写到主存,并把自己的状态从modify(修改)变成exclusive(独占)
  • 如果是无效,说明当前数据是被改过了,需要从主存重新读取最新的数据。

现在如果通知其他CPU的话是同步操作,得等其他CPU确认后再继续。

为了更高的性能,CPU又引入了修改缓冲,现在把最新修改的值写到「store buffer」中,并通知其他CPU记得要改状态,随后CPU就直接返回干其他事了。等到收到其它CPU发过来的响应消息,再将数据更新到高速缓存中。

其他CPU接收到invalid(无效)通知时,也会把接收到的消息放入「invalid queue」中,只要写到「invalid queue」就会直接返回告诉修改数据的CPU已经将状态置为「invalid」

CPU在读取的时候,需要去「store buffer」看看存不存在,存在则直接取,不存在才读主存的数据。

内存屏障

CPU1修改了A值,已把修改后值写到「store buffer」并通知CPU2对该值进行invalid(无效)操作,而CPU2可能还没收到invalid(无效)通知,就去做了其他的操作,导致CPU2读到的还是旧值。即便CPU2收到了invalid(无效)通知,但CPU1的值还没写到主存,那CPU2再次向主存读取的时候,还是旧值。

CPU对「缓存一致性协议」进行的异步优化「store buffer」「invalid queue」,很可能导致后面的指令很可能查不到前面指令的执行结果(各个指令的执行顺序非代码执行顺序),这种现象很多时候被称作「CPU乱序执行

为了解决乱序问题(可见性),又引出了「内存屏障」的概念。

内存屏障 为了解决 异步优化 导致 CPU乱序执行或者称 缓存不及时可见 的问题,把异步优化给禁用掉

内存屏障可以分为三种类型:写屏障,读屏障以及全能屏障(包含了读写屏障)

屏障可以简单理解为:在操作数据的时候,往数据插入一条”特殊的指令”。只要遇到这条指令,那前面的操作都得「完成」。

写屏障就可以这样理解:CPU当发现写屏障的指令时,会把该指令「之前」存在于「store Buffer」所有写指令刷入高速缓存。

让CPU修改的数据可以马上暴露给其他CPU,达到「写操作」可见性的效果。

读屏障也是类似的:CPU当发现读屏障的指令时,会把该指令「之前」存在于「invalid queue」所有的指令都处理掉,这种方式就可以确保当前CPU的缓存状态是准确的,达到「读操作」一定是读取最新的效果。

JMM

由于不同CPU架构的缓存体系不一样、缓存一致性协议不一样、重排序的策略不一样、所提供的内存屏障指令也有差异,为了简化Java开发人员的工作。Java封装了一套规范,这套规范就是「Java内存模型」。

「Java内存模型」希望 屏蔽各种硬件和操作系统的访问差异,保证了Java程序在各种平台下对内存的访问都能得到一致效果。目的是解决多线程存在的原子性、可见性(缓存一致性)以及有序性问题。

内存模型

线程之间的「共享变量」存储在「主内存」中,每个线程都有自己私有的「本地内存」,「本地内存」存储了该线程以读/写共享变量的副本。 

Java内存模型规定了:线程对变量的所有操作都必须在「本地内存」进行,「不能直接读写主内存」的变量

 Java内存模型定义了8个原子操作read/load/use/assign/store/write/lock/unlock 实现 变量如何从主内存到本地内存,以及变量如何从本地内存到主内存。

volatile 

可见性和有序性(禁止重排序)

实现

Java内存模型为了实现volatile有序性和可见性,定义了4种内存屏障的「规范」,分别是LoadLoad/LoadStore/StoreLoad/StoreStore。

LoadLoad Barriers

示例:Load1; LoadLoad; Load2

该屏障确保Load1数据的装载先于Load2及其后所有装载指令的的操作

StoreStore Barriers

示例:Store1; StoreStore; Store2

该屏障确保Store1立刻刷新数据到内存(使其对其他处理器可见)的操作先于Store2及其后所有存储指令的操作

LoadStore Barriers

示例:Load1; LoadStore; Store2

确保Load1的数据装载先于Store2及其后所有的存储指令刷新数据到内存的操作

StoreLoad Barriers

示例:Store1; StoreLoad; Load2

该屏障确保Store1立刻刷新数据到内存的操作先于Load2及其后所有装载装载指令的操作。它会使该屏障之前的所有内存访问指令(存储指令和访问指令)完成之后,才执行该屏障之后的内存访问指令。
 

就是在volatile「前后」加上「内存屏障」,使得编译器和CPU无法进行重排序,致使有序,并且写volatile变量对其他线程可见。

Hotspot虚拟机的实现,在「汇编」层面上实际是通过Lock前缀指令来实现的。

lock指令能保证:禁止CPU和编译器的重排序(保证了有序性)、保证CPU写核心的指令可以立即生效且其他核心的缓存数据失效(保证了可见性)。

Happens-Before

  1. 程序次序规则(Program Order Rule):在一个线程内,按照控制流顺序,书写在前面的操作先行发生(Happens-before)于书写在后面的操作。注意,这里说的是控制流顺序而不是程序代码顺序,因为要考虑分支、循环等结构。
  2. 管程锁定规则(Monitor Lock Rule):一个 unlock 操作先行发生于后面对同一个锁的 lock 操作。这里必须强调的是 “同一个锁”,而 “后面” 是指时间上的先后。
  3. volatile 变量规则(Volatile Variable Rule):对一个 volatile 变量的写操作先行发生于后面对这个变量的读操作,这里的 “后面” 同样是指时间上的先后。
  4. 线程启动规则(Thread Start Rule):Thread 对象的 start() 方法先行发生于此线程的每一个动作。
  5. 线程终止规则(Thread Termination Rule):线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过 Thread 对象的 join() 方法是否结束、Thread 对象的 isAlive() 的返回值等手段检测线程是否已经终止执行。
  6. 线程中断规则(Thread Interruption Rule):对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过 Thread 对象的 interrupted() 方法检测到是否有中断发生。
  7. 对象终结规则(Finalizer Rule):一个对象的初始化完成(构造函数执行结束)先行发生于它的 finalize() 方法的开始。
  8. 传递性(Transitivity):如果操作 A 先行发生于操作 B,操作 B 先行发生于操作 C,那就可以得出操作 A 先行发生于操作 C 的结论。

一个操作 “时间上的先发生” 不代表这个操作会是 “先行发生(Happens-before)”

参考

70-大厂面试火箭计划-Java并发/50-JMM最最最核心的概念-Happens-before原则.md · 小牛肉/CS-Wiki - Gitee.com

深入浅出Java内存模型 | 对线面试官

 为什么需要Java内存模型 | 对线面试官

https://www.zhihu.com/question/325469611/answer/1650954047

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值