文章目录
Java 内存模型(JMM)相关概念
Java 虚拟机规范之 Java 内存模型规范
Java 虚拟机规范中定义了 Java 内存模型(Java Memory Model,JMM)。JMM 是 Java 虚拟机规范的一部分,定义了多线程环境下对内存的访问规则
。主要是用于屏蔽掉各种硬件和操作系统的内存访问差异,以实现让 Java 程序在各种不同平台下都能达到一致的并发效果。JMM 规范统一了 Java 虚拟机与计算机内存是如何协同工作的,更好地控制并发行为,避免常见的并发问题,使得开发者可以在不同的平台上编写可以正常工作的 Java 程序,而不必担心底层硬件和操作系统的差异。
主要关注以下几个方面(多线程的三个特性):
-
原子性:
- 是指一个操作或者一系列操作要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
-
可见性:
- JMM 规定了一个线程如何以及何时可以看到其他线程对共享变量修改后的值。即当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
-
有序性:
-
JMM 规定了操作的执行顺序,确保线程内的操作按照代码的书写顺序执行,但在不同线程之间,操作的执行顺序可能会被重排序以提高性能。
程序执行的顺序按照代码的先后顺序执行。多线程中为了提高性能,
编译器
和处理器
常常会对指令做重排优化(编译器优化重排、指令并行重排、内存系统重排)。代码的执行顺序未必就是编写代码时候的顺序。但是这个重排序只是对单个线程内程序执行结果没有影响,在多线程环境下可能就有影响了。(java 会保证单线程无论如何重排序,程序最终执行结果都是一致的。而多线程就不能保证。即重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。)无论怎么重排序,Java 编译器和处理器都会保证在单线程下程序最终执行结果和代码顺序执行的结果是一致的,遵循 as-if-serial 语意。as-if-serial 语义主要适用于单线程中。编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作就可能被编译器和处理器重排序。
-
happens-before 语义规范
重排序
-
编译器优化重排序:编译器可以通过分析语句之间的数据依赖关系,优化无依赖的语句,使程序运行更高效。在不改变单线程的执行结果的情况下,可以重新安排语句的执行顺序。
// 编译器可能会重排 a 和 b 的赋值语句 int a = 1; int b = 2; int c = a + b;
-
处理器指令并行重排序:现代处理器可以在执行指令时进行乱序执行,以提高指令吞吐量。在不改变单线程的执行结果的情况下,可以采用指令级并行技术来将多条指令重叠执行。即如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
# 如果 A 和 B 不存在依赖关系,处理器可以将 LOAD R2, B 提前执行。 LOAD R1, A LOAD R2, B ADD R3, R1, R2
-
内存系统重排序:由于使用了缓存和缓冲区,使得加载和存储操作看上去就是乱序执行。
当一个线程写入一个变量时,该写入可能会被暂时存储在缓存中,而不是即时写入主内存。这可能导致其他线程在读取时看到旧的数据。
数据依赖性:
- 编译器和处理器在重排序时,会遵守数据依赖性原则,即编译器和处理器不会改变存在数据依赖关系的两个操作的执行顺序。(这里所说的数据依赖性仅针对单个处理器中执行的指令序列和单个线程中执行的操作,不同处理器之间和不同线程之间的数据依赖性不被编译器和处理器考虑)。
happens-before 关系
前面讲了通过遵循 as-if-serial 语义来保证单线程内程序的执行结果不被改变,那对于多线程遵循什么规则来保证并发编程的正确性。happens-before 规则就是多线程需要遵顼的,从而保证正确同步的多线程程序的执行结果不被改变。
从 JDK 5开始,Java 使用新的 JSR-133 内存模型,JSR-133 使用 happens-before 的概念来指定两个操作之间的执行顺序。JMM 可以通过 happens-before 关系向程序员提供跨线程的内存可见性保证。在 JMM 中,如果一个操作执行的结果需要对另一个操作可见(两个操作既可以是在一个线程之内,也可以是在不同线程之间),那么这两个操作之间必须要存在 happens-before 关系。
happens-before 定义:
- 如果一个操作 happens-before(先行发生于)另一个操作,那么第一个操作的结果对第二个操作是可见的,且第一个操作一定会在第二个操作之前执行。
happens-before 具体规则:
-
程序顺序规则:在一个线程内一段代码的执行结果是有序的。无论怎么重排序,结果是按照我们代码的顺序生成的不会变;
确保在单线程环境中,程序的执行结果一致。
-
锁定规则:一个线程对一个锁的释放先行发生于另一个线程对同一个锁的获取。后一个线程获取了这个锁都能看到前一个线程的操作结果。(管程是一种通用的同步原语,synchronized就是管程的实现);
确保在多线程环境中,锁的使用使得一个线程对共享变量的修改对其他线程可见。
synchronized (lock) { // 线程A的操作 } // 线程B获取同一个锁java synchronized (lock) { // 线程B能看到线程A的操作结果 }
-
volatile 变量规则:对一个 volatile 变量的写操作先行发生于后续对这个变量的读操作。写的结果对读线程可见;
使用
volatile
关键字可以确保一个线程对volatile
变量的写入对其他线程可见,避免了由于缓存造成的可见性问题。volatile int flag = 0; flag = 1; // 写操作 // 另一个线程读取 if (flag == 1) {