Java 内存模型是 Java 虚拟机(JVM)的一部分,用来定义多线程程序在共享内存中的交互规则。它为线程间的内存可见性、重排序、同步等提供了规范,以确保多线程程序的正确性和一致性。
1. JMM 的核心目标
- 线程间的共享变量可见性:线程修改共享变量时,其他线程何时能够看到修改结果。
- 指令重排序规则:编译器和处理器可以优化指令执行顺序,但不能违反程序的语义。
- 保证原子性、可见性和有序性:Java 提供了工具(如 volatile、锁、final 变量)来满足这些需求。
2. 内存结构
JMM 将内存划分为以下两个主要区域:
- 主内存(Main Memory):
- 存储所有的共享变量(实例变量、静态变量)。
- 所有线程都可以访问,但访问通常较慢。
- 工作内存(Working Memory):
- 每个线程独有的区域(类似 CPU 缓存)。
- 包含线程从主内存拷贝的共享变量副本。
- 线程对变量的操作必须在工作内存中进行,完成后再写回主内存。
3. JMM 的基本规则
- 线程对变量的读/写操作必须经过自己的工作内存:
- 线程不能直接读写主内存中的变量,必须先加载到工作内存。
- 线程修改的共享变量必须写回主内存:
- 写回后,其他线程才能看到最新的值。
线程间通信示意图:
lua
主内存
|
+--> 工作内存1 (线程1)
+--> 工作内存2 (线程2)
4. JMM 的特性
- 原子性
- JMM 保证基本数据类型的读/写是原子操作。
- 非原子操作需要借助 synchronized 或 Atomic 类。
- 可见性
- 一个线程修改共享变量后,其他线程能否立即看到。
- 工具:
- volatile:保证变量的可见性,但不保证原子性。
- synchronized 和 Lock:同步块或方法将工作内存中的值刷新到主内存。
- 有序性
- 程序的执行顺序可以被编译器和 CPU 优化,但在单线程环境下,程序表现为顺序一致性。
- 工具:
- volatile 禁止指令重排序。
- synchronized 保证代码块执行的顺序。
5. Happens-Before 原则
JMM 定义了一组规则,用于判断线程间操作的先后顺序。若一个操作 A happens-before 操作 B,则 A 的结果对 B 可见。
常见的 happens-before 关系:
- 程序次序规则:单线程中,程序按代码顺序执行。
- 锁定规则:一个锁的解锁操作 happens-before 该锁的加锁操作。
- volatile 规则:对 volatile 变量的写操作 happens-before 该变量的读操作。
- 传递性:如果 A happens-before B,且 B happens-before C,则 A happens-before C。
6. JMM 关键工具
- volatile:
- 保证可见性和有序性。
- 禁止指令重排序。
- 不保证原子性(需要结合 CAS 或锁)。
- synchronized:
- 保证原子性、可见性和有序性。
- 阻止线程间的并发冲突。
- final:
- 在对象构造时,final 修饰的字段一旦被初始化,其他线程可以安全读取。
7. 指令重排序与内存屏障
指令重排序:
编译器和处理器为了优化性能,会调整指令的执行顺序,但需遵守 happens-before 规则。
内存屏障:
JMM 使用内存屏障(Memory Barrier)限制指令的重排序,以确保多线程环境中的正确性。
8. 示例:JMM 中的可见性问题
问题代码:
java
class Example {
private static boolean stop = false;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while (!stop) {
// busy waiting
}
});
t1.start();
Thread.sleep(1000);
stop = true; // 主线程修改 stop
}
}
问题描述:
- 主线程修改 stop 后,线程 t1 可能无法立即感知,导致死循环。
- 原因:stop 的值未及时刷新到主内存。
解决方案:
- 使用 volatile:
java
private static volatile boolean stop = false;
总结
Java 内存模型提供了一种抽象机制,确保多线程程序的安全性和可预测性。开发者需要熟练掌握原子性、可见性、有序性以及工具(如 volatile 和 synchronized),才能在多线程编程中正确使用 JMM,避免并发问题。
内存屏障(Memory Barrier)
内存屏障,也称为内存栅栏,是一种 CPU 指令,用于控制指令和内存操作的执行顺序。其作用是防止指令重排序,并确保多核处理器环境中共享变量的内存可见性。
在现代计算机系统中,CPU 和编译器为了提高性能,可能会对指令进行乱序执行或指令重排序。内存屏障通过引入强制性规则,确保某些内存操作的顺序和可见性满足特定的要求。
内存屏障的作用
- 防止指令重排序:保证在内存屏障前后的指令按照程序的逻辑顺序执行。
- 保证内存可见性:确保对共享变量的修改在多核 CPU 上能及时被其他处理器看到。
- 同步线程间的内存操作:确保线程之间对共享数据的操作一致性。
分类
内存屏障主要分为以下几种类型:
- Load Barrier(加载屏障)
- 保证屏障之前的所有内存读取操作(load)都完成后,屏障之后的读取操作才能开始。
- 用途:确保读取操作的结果在多线程环境中是最新的。
- Store Barrier(存储屏障)
- 保证屏障之前的所有内存写入操作(store)都完成后,屏障之后的写入操作才能开始。
- 用途:确保写入操作在其他线程中可见。
- Full Barrier(全屏障)
- 同时阻止屏障前的读取和写入操作与屏障后的读取和写入操作重排序。
- 用途:确保屏障前的所有内存操作都完成,屏障后的所有内存操作才开始。
- Acquire Barrier 和 Release Barrier
- Acquire Barrier:阻止屏障后的操作重排序到屏障前,用于内存读取。
- Release Barrier:阻止屏障前的操作重排序到屏障后,用于内存写入。
内存屏障的应用场景
-
指令重排序问题
现代 CPU 和编译器可能会为了性能优化而调整指令的执行顺序,但这种优化可能导致多线程程序的执行结果不符合预期。内存屏障可以强制保证程序中某些关键操作的顺序。
-
多线程可见性问题
在多线程环境中,一个线程对共享变量的修改可能不会立即被另一个线程看到。这是因为每个线程有自己的缓存(如 CPU 缓存),内存屏障可以确保线程间的可见性。
Java 和内存屏障
在 Java 中,内存屏障通常由 JVM 和硬件自动插入,开发者并不直接使用。但是以下工具会触发内存屏障:
- volatile
- 规则:
- 对 volatile 变量的写操作会插入Store Barrier。
- 对 volatile 变量的读操作会插入Load Barrier。
- 作用:确保 volatile 变量在多线程环境中的可见性和有序性。
- 规则:
- synchronized
- 进入同步块:
- 隐式插入Acquire Barrier,保证同步块内的操作不会被重排序到同步块外。
- 退出同步块:
- 隐式插入Release Barrier,确保同步块内的所有操作结果对其他线程可见。
- 进入同步块:
硬件层面的内存屏障
x86/64 架构
- x86/64 架构通常提供以下指令用于内存屏障:
- LFENCE:Load Barrier。
- SFENCE:Store Barrier。
- MFENCE:Full Barrier。
ARM 架构
- ARM 架构更为宽松,提供以下指令:
- DMB:数据内存屏障。
- DSB:数据同步屏障。
- ISB:指令同步屏障。
内存屏障的例子
1. 多线程可见性
以下代码展示了一个线程修改共享变量,而另一个线程可能无法看到更新的问题:
java
class Example {
private static boolean stop = false;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while (!stop) {
// busy waiting
}
});
t1.start();
Thread.sleep(1000);
stop = true; // 主线程修改 stop,但可能无法立即被线程 t1 感知
}
}
解决方案:使用 volatile
java
private static volatile boolean stop = false;
- 对 stop 的写操作会插入 Store Barrier。
- 对 stop 的读操作会插入 Load Barrier。
总结
内存屏障是控制多线程环境中内存操作顺序和可见性的重要机制。它在硬件、操作系统和编程语言(如 Java)中都有重要应用。通过使用 volatile、synchronized 等关键字,我们可以在高层次上使用内存屏障,而无需直接操作底层指令。