Java内存模型(JMM)与Happens-Before规则详解
一、为什么需要内存模型?
1. 硬件层面的挑战
硬件特性 | 引发的问题 | JMM的解决方案 |
---|---|---|
CPU缓存一致性 | 多核缓存数据不一致 | 定义主内存与工作内存交互规则 |
指令重排序 | 程序执行顺序与代码顺序不一致 | Happens-Before规则约束 |
多级存储架构 | 内存访问速度差异显著 | 内存屏障指令控制可见性 |
2. 并发编程三要素
graph TD
A[原子性] -->|synchronized| B(操作不可中断)
C[可见性] -->|volatile| D(修改立即可见)
E[有序性] -->|Happens-Before| F(禁止特定重排序)
二、JMM核心结构解析
1. 内存交互八大操作
操作类型 | 作用 | 等效代码示例 |
---|---|---|
lock | 锁定主内存变量 | synchronized(obj) { |
read | 从主内存读取到工作内存 | int localVar = sharedVar; |
load | 将read值放入工作内存副本 | |
use | 线程使用变量值 | if (localVar > 0) {...} |
assign | 给工作内存变量赋值 | localVar = 42; |
store | 将工作内存值传回主内存 | |
write | 将store值写入主内存变量 | sharedVar = localVar; |
unlock | 释放主内存锁 | } |
2. 内存屏障类型(以x86为例)
java
// StoreStore屏障示例
public class MemoryBarrier {
int x;
volatile boolean v;
void writer() {
x = 42; // 普通写
StoreStore; // 隐式屏障(由volatile写入触发)
v = true; // volatile写
}
void reader() {
if (v) { // volatile读
LoadLoad; // 隐式屏障
System.out.println(x); // 保证看到x=42
}
}
}
三、Happens-Before规则全解
1. 规则列表与案例
规则名称 | 具体描述 | 代码示例 |
---|---|---|
程序顺序规则 | 同一线程内操作按代码顺序执行 | int a=1; int b=a; // b一定能看到a=1 |
volatile变量规则 | volatile写先于后续任意线程的读 | volatile int x; x=1; → 其他线程读x必见1 |
线程启动规则 | Thread.start()前操作对线程内可见 | int var=1; new Thread(()->System.out.println(var)).start(); // 必输出1 |
线程终止规则 | 线程所有操作先于其他线程检测到其终止 | t.join(); → 主线程可见t线程的所有修改 |
中断规则 | 线程interrupt()调用先于被中断线程检测中断 | thread.interrupt(); → thread 内isInterrupted() 必为true |
2. 传递性证明
java
class HBTransitivity {
int x = 0;
volatile boolean v = false;
void write() {
x = 42; // (1) 普通写
v = true; // (2) volatile写
}
void read() {
if (v) { // (3) volatile读
System.out.println(x); // 保证输出42
// (1) happens-before (2)
// (2) happens-before (3)
// 根据传递性 → (1) happens-before (3)
}
}
}
四、实战:DCL单例与volatile
1. 错误实现分析
java
class Singleton {
private static Singleton instance;
static Singleton getInstance() {
if (instance == null) { // 第一次检查(未同步)
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton(); // 可能发生指令重排序!
}
}
}
return instance;
}
}
问题根源 :new Singleton()
可能被重排序为:
- 分配内存空间
- 将引用指向内存(instance非null)
- 初始化对象(此时其他线程可能访问到未初始化的对象)
2. 正确解决方案
java
class SafeSingleton {
private static volatile SafeSingleton instance;
static SafeSingleton getInstance() {
if (instance == null) {
synchronized (SafeSingleton.class) {
if (instance == null) {
instance = new SafeSingleton(); // volatile禁止重排序
}
}
}
return instance;
}
}
volatile作用:
- 禁止步骤2与步骤3的重排序
- 保证修改对所有线程立即可见
五、常见问题QA
💬 Q1:final字段是否受JMM约束?
✅ 答案: final字段有特殊的内存语义:
- 正确构造的对象中,final字段初始化值对所有线程可见(无需同步)
- 但若对象引用逃逸(如构造函数中发布this引用),可能看到未初始化的final字段
💬 Q2:synchronized是否保证有序性?
✅ 答案: 是的!synchronized的解锁操作包含:
- 将工作内存刷新到主内存(Store)
- 插入StoreStore屏障(禁止写重排序)
- 插入StoreLoad屏障(保证后续读能看到最新值)
💬 Q3:x86架构为什么需要内存屏障?
✅ 答案: 虽然x86是强内存模型(天然保证load-load有序性),但仍需:
- StoreLoad屏障:防止store操作与后续load操作重排序(唯一需要显式插入的屏障)
- Lock前缀指令 :如volatile写实际编译为
lock addl $0,0(%rsp)
,隐含完整屏障
最佳实践:
- 优先使用
java.util.concurrent
工具类(已正确实现内存语义) - 调试工具推荐:
-XX:+PrintAssembly
查看汇编指令jconsole
观察线程内存状态
- 避免过度优化:99%的场景不需要手动插入内存屏障