内存模型(JMM)
什么是 JMM?
JMM 即 Java内存模型,是 java 虚拟机规范中的一个概念,定义了多线程环境下共享变量的访问规则以及不同线程之间如何通过内存进行交互。JMM 主要解决了 java 多线程环境下可能出现的可见性、原子性和有序性问题。在我看来,主要是对程序员的一种承诺,happens-before
就是这种承诺的表现形式。
JMM 对底层硬件的抽象
线程 A 与线程 B 之间通过主存进行交互,每个线程都有一个本地内存,其内部有共享变量的副本。所谓的“本地内存”只是 JMM 抽象出来的一个概念,实际上并不存在,它是对CPU缓存
,写缓冲区
,寄存器
的抽象。整个 JMM 可以认为是对底层硬件和编译器操作的优化和限制。
指令重排序
首先,你得知道:java 代码 => 字节码 => 机器码
总的来说可以分为三个部分:
- 编译器在不改变程序语义的前提下,即不改变最终的执行结果,可以改变代码的执行顺序
- 如果不存在数据依赖性,现代处理器大都会采用
指令级并行技术
来重叠多条指令执行 - 由于处理器缓冲区的存在,内存的读写看上去是异步执行的
为什么要将指令重排序?
提高 CPU 的并行处理能力。
你现在可能很懵逼,继续往下看!
as-if-serial 语义
该语言的含义是:无论怎么进行指令的重排序,单线程程序的执行结果都不能被改变。编译器和处理器都必须遵守该语义。
举个例子:
int a = 0;
int b = 0;
int c = a + b;
先读取 a 或者先读取 b 对最终的结果有影响嘛?没有!
内存屏障指令
指令都不用说,都知道。“内存屏障”是一个什么东西呢?
现在的计算机基本上都是多核处理器,即拥有多个 CPU 。每个 CPU 都有自己的读/写缓冲区,对应着 JMM 的线程本地内存(一个CPU同一时间只能跑一个线程)。操作自己缓冲区上的数据,对其他 CPU 是不可见的,即线程在本地内存中的操作是不可见的。这种现象就是所谓的“内存屏障”。
内存屏障指令就是用来打破这个规则的,让线程操作后的脏数据能够及时写回主存。
Happens - before 规则
happens-before
规则就是 JMM 对程序员承诺的一种通俗易懂的形式,我们可以不用去学习复杂的重排序规则以及规则的具体实现。
happens-before
翻译为“在...之前发生”,描述的是操作与操作之间的执行顺序及结果的可见性。这里重点关注几点:
- 程序的顺序规则:一个线程中的每个操作,
happens-before
于之后的任意操作 - 监视器锁规则:对一个监视器锁的解锁,
happens-before
于随后对这个监视器锁的加锁 - volatile 变量规则: 对一个 volatile 域的写,
happens-before
于任意后续对这个 volatile 域的读 - 传递性:如果 A happens- before B,且 B happens- before C,那么 A happens- before C
这里描述的操作可以是单线程的,也可以是多线程的。
(好好体会一下)
总线事务
在计算机中,数据通过总线在处理器和内存之间传递。每次处理器和内存之间的数据传递都是通过一系列步骤来完成的,这一系列步骤称之为总线事务(bus transaction)。总线事务包括读事务(read transaction)和写事务(write transaction)。读事务从内存传送数据到处理器,写事务从处理器传送数据到内存,每个事务会读 / 写内存中一个或多个物理上连续的字。这里的关键是,总线会同步试图并发使用总线的事务。在一个处理器执行总线事务期间,总线会禁止其它所有的处理器和 I/O 设备执行内存的读 / 写。下面让我们通过一个示意图来说明总线的工作机制:
总线的这些工作机制可以把所有处理器对内存的访问以串行化的方式来执行;在任意时间点,最多只能有一个处理器能访问内存。这个特性确保了单个总线事务之中的内存读 / 写操作具有原子性。
单次事务能传递的数据量大小主要受总线的宽度影响,总线宽度越宽,一次传输的数据位数就越多。例如,32位宽的总线一次可以传输4字节的数据,而64位宽的总线一次可以传输8字节的数据。
这也就意味着32位机器上对于64位数据类型的读写操作原子性实现开销较大,JVM 规范没有强制要求必须实现在 32 位机器上对于64为数据类型读写操作的原子性。
总结
今日份内容还是比较轻松,重点掌握 JMM 的思想。
2025/01/24
writeBy kaiven