Java中的Java内存模型(JMM)与happens-before原则详解
一、知识描述
Java内存模型(Java Memory Model, JMM)是Java虚拟机规范中定义的一种抽象模型,用于屏蔽各种硬件和操作系统的内存访问差异,确保Java程序在各种平台上都能得到一致的内存访问效果。JMM的核心问题是解决在多线程并发环境下,如何处理共享变量的可见性、原子性和有序性问题。happens-before原则是JMM中定义的一组规则,用于描述两个操作之间的内存可见性关系。
二、知识背景与必要性
- 硬件差异:现代计算机系统为了提升性能,普遍采用多级缓存、CPU乱序执行等技术,这导致了不同CPU对同一内存位置的读取可能得到不同结果
- 编译器优化:Java编译器(javac)和运行时编译器(JIT)会对指令进行重排序优化,可能改变代码的执行顺序
- 线程安全问题:在没有正确同步的情况下,多线程访问共享变量可能出现不可预期的结果
三、JMM的核心概念
-
主内存(Main Memory)
- 所有共享变量都存储在主内存中
- 主内存是所有线程共享的内存区域
-
工作内存(Working Memory)
- 每个线程有自己的工作内存,存储该线程使用到的变量的主内存副本
- 线程对变量的所有操作(读取、赋值)都在工作内存中进行
-
内存间交互操作
- lock(锁定):作用于主内存变量,标识为线程独占状态
- unlock(解锁):作用于主内存变量,释放锁定状态
- read(读取):从主内存传输变量值到线程工作内存
- load(载入):把read得到的值放入工作内存的变量副本中
- use(使用):把工作内存中的变量值传递给执行引擎
- assign(赋值):把从执行引擎接收的值赋给工作内存中的变量
- store(存储):把工作内存中的变量值传送到主内存
- write(写入):把store得到的值放入主内存的变量中
四、happens-before原则详解
happens-before关系定义了两个操作之间的偏序关系,如果操作A happens-before 操作B,那么A操作对内存的修改对B操作可见。
-
程序顺序规则(Program Order Rule)
- 在单个线程中,按照程序代码的顺序,前面的操作happens-before后面的操作
- 注意:这仅保证在单线程内的执行结果,在多线程环境下仍需考虑重排序
-
监视器锁规则(Monitor Lock Rule)
- 对一个锁的解锁操作happens-before随后对这个锁的加锁操作
synchronized (lock) { // 线程A在此区域内的所有写操作 sharedVariable = 1; // 操作A } // 解锁操作happens-before线程B的加锁操作 synchronized (lock) { // 线程B能看到线程A的写操作结果 System.out.println(sharedVariable); // 保证输出1 } -
volatile变量规则(Volatile Variable Rule)
- 对一个volatile变量的写操作happens-before后续对这个变量的读操作
- volatile变量能防止指令重排序,保证可见性
-
线程启动规则(Thread Start Rule)
- 线程的start()方法调用happens-before该线程的任何操作
int x = 10; Thread t = new Thread(() -> { // 此处能看到x=10,因为start()调用happens-beforerun()方法执行 System.out.println(x); // 保证输出10 }); x = 20; // 这个修改对线程t不可见 t.start(); -
线程终止规则(Thread Termination Rule)
- 线程中的所有操作都happens-before其他线程检测到该线程已经终止
Thread t = new Thread(() -> { sharedVariable = 100; // 操作A }); t.start(); t.join(); // 等待线程终止 // 此处保证能看到sharedVariable=100 -
中断规则(Interruption Rule)
- 对线程interrupt()方法的调用happens-before被中断线程检测到中断事件
-
对象终结规则(Finalizer Rule)
- 对象的构造函数执行结束happens-before它的finalize()方法的开始
-
传递性(Transitivity)
- 如果A happens-before B,且B happens-before C,那么A happens-before C
五、实际应用示例
public class HappensBeforeExample {
private int x = 0;
private volatile boolean flag = false;
public void writer() {
x = 42; // 操作1
flag = true; // 操作2:volatile写
}
public void reader() {
if (flag) { // 操作3:volatile读
System.out.println(x); // 操作4:保证输出42
}
}
}
分析:
- 根据程序顺序规则:操作1 happens-before 操作2
- 根据volatile规则:操作2 happens-before 操作3
- 根据程序顺序规则:操作3 happens-before 操作4
- 根据传递性:操作1 happens-before 操作4,因此x=42对操作4可见
六、JMM的内存屏障
为了实现happens-before关系,JMM在底层使用了内存屏障指令:
- LoadLoad屏障:确保Load1的数据装载在Load2及后续装载指令之前
- StoreStore屏障:确保Store1的数据对其他处理器可见在Store2及后续存储指令之前
- LoadStore屏障:确保Load1的数据装载在Store2及后续存储指令之前
- StoreLoad屏障:确保Store1的数据对其他处理器可见在Load2及后续装载指令之前
七、总结
Java内存模型通过定义happens-before关系,为开发者提供了一套保证多线程程序正确性的规则。理解JMM和happens-before原则对于编写正确的并发程序至关重要,它帮助我们理解在什么情况下一个线程的写操作对另一个线程可见,从而避免出现内存可见性问题。