Java 内存模型(JMM)主要通过 禁止指令重排序 和 建立 happens-before 规则 来保证有序性,核心手段包括 volatile 关键字、synchronized 关键字和 final 关键字等。
一、有序性问题的根源
有序性问题的根源是 CPU 指令重排序。为了优化性能,CPU 会在不影响单线程执行结果的前提下,调整指令的执行顺序。例如:
ini
int a = 1; // 操作 1
int b = 2; // 操作 2
int c = a + b; // 操作 3
单线程下,CPU 可能调整为操作 2 → 操作 1 → 操作 3,结果依然是 c=3,不影响正确性。但多线程下,重排序可能破坏依赖关系,导致错误。
二、JMM 保证有序性的核心机制
1. 内存屏障(Memory Barrier)
JMM 规定了 内存屏障 来禁止特定类型的指令重排序。内存屏障是一种特殊的 CPU 指令,它会强制 CPU 执行完屏障之前的所有指令,再执行屏障之后的指令,同时确保屏障前后的内存操作可见。
常见的内存屏障类型:
| 屏障类型 | 作用 |
|---|---|
| LoadLoad 屏障 | 确保屏障前的加载指令(Load)先于屏障后的加载指令执行。 |
| StoreStore 屏障 | 确保屏障前的存储指令(Store)先于屏障后的存储指令执行。 |
| LoadStore 屏障 | 确保屏障前的加载指令先于屏障后的存储指令执行。 |
| StoreLoad 屏障 | 确保屏障前的存储指令先于屏障后的加载指令执行(最严格,会刷新缓存)。 |
2. volatile 关键字的有序性保证
volatile 关键字通过 插入内存屏障 来禁止重排序。JMM 规定:
- 当变量被
volatile修饰时,编译器和 CPU 必须在该变量的读写操作前后插入特定的内存屏障,防止其与其他指令重排序。
具体规则:
- 写操作 :在
volatile变量的写操作后插入 StoreStore 屏障 和 StoreLoad 屏障,确保写操作对其他线程可见。 - 读操作 :在
volatile变量的读操作前插入 LoadLoad 屏障 和 LoadStore 屏障,确保读操作能获取到最新的值。
示例:
ini
volatile int flag = 0;
// 线程 1
flag = 1; // 写操作,插入 StoreStore + StoreLoad 屏障
// 线程 2
if (flag == 1) { // 读操作,插入 LoadLoad + LoadStore 屏障
// ...
}
volatile 禁止了 flag 变量的读写操作与其他指令的重排序,保证了多线程下的有序性。
3. synchronized 关键字的有序性保证
synchronized 关键字通过 互斥执行 和 happens-before 规则 保证有序性。
- 互斥执行 :
synchronized修饰的同步块同一时间只能被一个线程执行,因此同步块内的指令不会与其他线程的指令交错执行,间接保证了有序性。 - happens-before 规则:
- 线程 A 释放锁时的操作 happens-before 线程 B 获取锁后的操作。
- 这意味着线程 A 在同步块内的所有操作,对线程 B 都是可见的,且执行顺序不会被重排序。
示例:
ini
synchronized (lock) {
// 同步块内的指令不会被重排序,且对其他线程可见
a = 1;
b = 2;
}
4. final 关键字的有序性保证
final 关键字通过 禁止重排序初始化过程 来保证有序性。
- 对于
final修饰的变量,编译器会确保:
- 变量的初始化完成后,才能被其他线程访问。
- 禁止将
final变量的初始化与其他指令重排序,避免出现"半初始化"状态。
示例:
arduino
public class FinalExample {
private final int x;
public FinalExample() {
x = 1; // final 变量初始化
}
}
其他线程访问 x 时,x 一定已经被初始化为 1,不会出现 x 未初始化的情况。
5. happens-before 规则
JMM 定义了 happens-before 规则 ,用于判断多线程操作的执行顺序。如果操作 A happens-before 操作 B,则:
- 操作 A 的执行结果对操作 B 可见。
- 操作 A 的执行顺序在操作 B 之前。
常用的 happens-before 规则:
| 规则 | 说明 |
|---|---|
| 程序顺序规则 | 同一线程内,按照代码顺序,前面的操作 happens-before 后面的操作。 |
volatile 变量规则 |
volatile 变量的写操作 happens-before 后续的读操作。 |
| 锁规则 | 锁的释放操作 happens-before 后续的获取锁操作。 |
| 线程启动规则 | Thread.start() 方法 happens-before 线程内的所有操作。 |
| 线程终止规则 | 线程内的所有操作 happens-before 线程的终止检测(如 Thread.join())。 |
| 传递性规则 | 如果 A happens-before B,且 B happens-before C,则 A happens-before C。 |
总结
JMM 保证有序性的核心手段是 禁止指令重排序 和 建立 happens-before 规则:
- 禁止重排序 :通过
volatile、synchronized、final等关键字插入内存屏障,限制 CPU 和编译器的重排序行为。 - happens-before 规则:通过定义操作之间的偏序关系,确保多线程环境下操作的可见性和有序性。
理解这些机制,是编写安全、高效的并发程序的基础。