为了账号安全,请及时绑定邮箱和手机立即绑定

并发编程之 Java 内存模型 + volatile 关键字 + Happen-Before 规则

标签:
Java

前言

楼主这个标题其实有一种作死的味道,为什么呢,这三个东西其实可以分开为三篇文章来写,但是,楼主认为这三个东西又都是高度相关的,应当在一个知识点中。在一次学习中去理解这些东西。才能更好的理解 Java 内存模型和 volatile 关键字还有 HB 原则。

如果觉得对你有所帮助可以加入我的交流学习群一起学习哦 118599747

楼主今天就尝试着在一篇文章中讲述这三个问题,最后总结。

讲并发知识前必须复习的硬件知识。

Java 内存模型到底是什么玩意?

Java 内存模型定义了哪些东西?

Java内存模型引出的 Happen-Before 原则是什么?

Happen-Before 引出的 volatile 又是什么?

总结这三者。

1. 讲并发知识前必须复习的硬件知识。

首先,因为我们需要了解 Java 虚拟机的并发,而物理硬件的并发和虚拟机的并发很相似,而且虚拟机的并发很多看着奇怪的设计都是因为物理机的设计导致的。

什么是并发?多个CPU同时执行。但请注意:只有CPU是不行的,CPU 只能计算数据,那么数据从哪里来?

答案:内存。 数据从内存中来。需要读取数据,存储计算结果。有的同学可能会说,不是有寄存器和多级缓存吗?但是那是静态随机访问内存(Static Random Access Memory),太贵了,SRAM 在设计上使用的晶体管数量较多,价格较高,且不易做成大容量,只能用很小的部分集成的CPU中成为CPU的高速缓存。而正常使用的都是都是动态随机访问内存(Dynamic Random Access Memory)。intel 的 CPU 外频 需要从北桥经过访问内存,而AMD 的没有设计北桥,他与 Intel 不同的地方在于,内存是直接与CPU通信而不通过北桥,也就是将内存控制组件集成到CPU中。理论上这样可以加速CPU和内存的传输速度。

好了,不管哪一家的CPU,都需要从内存中读取数据,并且自己都有高速缓存或者说寄存器。缓存作什么用呢?由于CPU的速度很快,内存根本跟不上CPU,因此,需要在内存和CPU直接加一层高速缓存让他们缓冲CPU的数据:将运算需要使用到的数据复制到缓存中,让运算能够快速执行,当运算结束后再从缓存同步到内存之中。这样处理器就无需等待缓慢的内存读写了。

webp

但是这样引出了另一个问题:缓存一致性(Cache Coherence)。什么意思呢?

在多处理器中,每个处理器都有自己的高速缓存,而他们又共享同一个主内存(Main Memory),当多个处理器的运算任务都涉及到同一块主内存区域时,将可能导致各自的缓存数据不一致。如果真的发生这种情况,拿同步到主内存时以谁的缓存数据为准呢?

在早期的CPU当中,可以通过在总线上加 LOCK# 锁的形式来解决缓存不一致的问题。因为CPU和其他部件进行通信都是通过总线来进行的,如果对总线加LOCK#锁的话,也就是说阻塞了其他CPU对其他部件访问(如内存),从而使得只能有一个CPU能使用这个变量的内存。

现在的 CPU 为了解决一致性问题,需要各个CPU访问(读或者写)缓存的时候遵循一些协议:MSI,MESI,MOSI,Synapse,Firefly,Dragon Protocol,这些都是缓存一致性协议。

那么,这个时候需要说一个名词:内存模型。

什么是内存模型呢?

内存模型可以理解为在特定的操作协议下,对特定的内存或高速缓存进行读写访问的过程抽象。不同架构的CPU 有不同的内存模型,而 Java 虚拟机屏蔽了不同CPU内存模型的差异,这就是Java 的内存模型。

那么 Java 的内存模型的结构是什么样子的呢?

webp

好了,关于为什么会有内存模型这件事,我们已经说的差不多了,总体来说就是因为多个CPU的多级缓存访问同一个内存条可能会导致数据不一致。所以需要一个协议,让这些处理器在访问内存的时候遵守这些协议保证数据的一致性。

还有一个问题。CPU 的流水线执行和乱序执行

我们假设我们现在有一段代码:

int a = 1; int b = 2; int c = a + b;

上面的代码我们能不能不顺序动一下并且结果不变呢?可以,第一行和第二行调换没有任何问题。

实际上,CPU 有时候为了优化性能,也会对代码顺序进行调换(在保证结果的前提下),专业术语叫重排序。为什么重排序会优化性能呢?

这个就有点复杂了,我们慢慢说。

我们知道,一条指令的执行可以分为很多步骤的,简单的说,可以分为以下几步:

取指 IF

译码和取寄存器操作数 ID

执行或者有效地址计算 EX

存储器返回 MEM

写回 WB

我们的汇编指令也不是一步就可以执行完毕的,在CPU 中实际工作时,他还需要分为多个步骤依次执行,每个步骤涉及到的硬件也可能不同,比如,取指时会用到 PC 寄存器和存储器,译码时会用到指令寄存器组,执行时会使用 ALU,写回时需要寄存器组。

也就是说,由于每一个步骤都可能使用不同的硬件完成,因此,CPU 工程师们就发明了流水线技术来执行指令。什么意思呢?

假如你需要洗车,那么洗车店会执行 “洗车” 这个命令,但是,洗车店会分开操作,比如冲水,打泡沫,洗刷,擦干,打蜡等,这写动作都可以由不同的员工来做,不需要一个员工依次取执行,其余的员工在那干等着,因此,每个员工都被分配一个任务,执行完就交给下一个员工,就像工厂里的流水线一样。

CPU 在执行指令的时候也是这么做的。

既然是流水线执行,那么流水线肯定不能中断,否则,一个地方中断会影响下游所有的组件执行效率,性能损失很大。

那么怎么办呢?打个比方,1冲水,2打泡沫,3洗刷,4擦干,5打蜡 本来是按照顺序执行的。如果这个时候,水没有了,那么冲水后面的动作都会收到影响,但是呢,其实我们可以让冲水先去打水,和打泡沫的换个位置,这样,我们就先打泡沫,冲水的会在这个时候取接水,等到第一辆车的泡沫打完了,冲水的就回来了,继续赶回,不影响工作。这个时候顺序就变成了:

1打泡沫 ,2冲水,3洗刷,4擦干,5打蜡.

但是工作丝毫不受影响。流水线也没有断。CPU 中的乱序执行其实也跟这个道理差不多。其最终的目的,还是为了压榨 CPU 的性能。

好了,对于今天的文章需要的硬件知识,我们已经复习的差不多了。总结一下,主要是2点:

CPU 的多级缓存访问主存的时候需要配合缓存一致性协议。这个过程可以抽象为内存模型。

CPU 为了性能会让指令流水线执行,并且会在单个 CPU 的执行结构不混乱的情况下乱序执行。

那么,接下来就要好好说说Java 的内存模型了。



作者:IT小阿彪
链接:https://www.jianshu.com/p/c9250583274c


点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消