深入理解Java内存模型(JMM)
// 目录 · contents
1. 引言
当我们编写多线程程序时,一个看似正确的代码可能在运行时产生意想不到的结果。这并非程序逻辑有误,而是因为现代处理器和编译器为了提升性能会对指令进行重排序,而Java内存模型(Java Memory Model, JMM)正是定义了在这种复杂环境下,多线程程序如何正确地共享数据。
JMM是理解Java并发编程的理论基础。不理解JMM,就很难真正理解volatile、synchronized、final这些关键字在并发环境下的行为。
2. JMM的结构与抽象
2.1 主内存与工作内存
JMM将内存抽象为主内存(Main Memory)和工作内存(Working Memory)两个层次:
graph TB
subgraph 主内存 Main Memory
MV[共享变量]
end
subgraph 线程1工作内存
WM1[变量副本1]
end
subgraph 线程2工作内存
WM2[变量副本2]
end
subgraph 线程3工作内存
WM3[变量副本3]
end
WM1 <-->|read/write| MV
WM2 <-->|read/write| MV
WM3 <-->|read/write| MV
style MV fill:#FFD700,stroke:#333
style WM1 fill:#87CEEB
style WM2 fill:#87CEEB
style WM3 fill:#87CEEB
- 主内存:所有线程共享的内存区域,存储共享变量的”官方值”
- 工作内存:每个线程私有,存储该线程使用的共享变量副本
线程对变量的所有操作(读取、赋值)都必须在工作内存中进行,不能直接操作主内存。线程间变量值的传递必须通过主内存完成。
2.2 内存交互操作
JMM定义了8种原子操作来完成主内存与工作内存之间的交互:
sequenceDiagram
participant WM as 工作内存
participant MM as 主内存
Note over MM: lock (锁定)
MM->>WM: read (读取) + load (载入)
Note over WM: use (使用) - 传给执行引擎
Note over WM: assign (赋值) - 执行引擎写回
WM->>MM: store (存储) + write (写入)
Note over MM: unlock (解锁)
| 操作 | 作用域 | 说明 |
|---|---|---|
| lock | 主内存 | 将变量标识为线程独占状态 |
| unlock | 主内存 | 释放锁定的变量 |
| read | 主内存 | 将变量值从主内存传输到工作内存 |
| load | 工作内存 | 将read到的值放入工作内存的变量副本 |
| use | 工作内存 | 将工作内存中的值传给执行引擎 |
| assign | 工作内存 | 将执行引擎的值赋给工作内存中的变量 |
| store | 工作内存 | 将工作内存中的变量值传送到主内存 |
| write | 主内存 | 将store传来的值写入主内存变量 |
3. 重排序
为了提升执行效率,编译器和处理器会对指令进行重排序。重排序分为三种类型:
graph LR
A[源代码] -->|1.编译器优化重排序| B[编译后指令]
B -->|2.指令级并行重排序| C[处理器执行序列]
C -->|3.内存系统重排序| D[最终执行效果]
style A fill:#E8F5E8
style D fill:#FFE4E1
- 编译器优化重排序:编译器在不改变单线程语义的前提下重新安排语句执行顺序
- 指令级并行重排序:现代处理器使用指令级并行技术将多条指令重叠执行
- 内存系统重排序:处理器使用写缓冲区、无效化队列,导致加载和存储操作看起来被重排序
3.1 重排序带来的问题
1 | |
4. happens-before规则
JMM通过happens-before关系来保证跨线程的内存可见性。如果操作A happens-before操作B,那么A的执行结果对B可见,且A的执行顺序排在B之前(从内存可见性角度)。
4.1 八大happens-before规则
graph TD
HB[happens-before规则] --> R1[1. 程序顺序规则]
HB --> R2[2. 监视器锁规则]
HB --> R3[3. volatile变量规则]
HB --> R4[4. 线程启动规则]
HB --> R5[5. 线程终止规则]
HB --> R6[6. 线程中断规则]
HB --> R7[7. 对象终结规则]
HB --> R8[8. 传递性规则]
R1 -->|"同一线程中前面的操作<br/>hb后面的操作"| E1[单线程内有序]
R2 -->|"unlock hb<br/>后续的lock"| E2[锁的可见性]
R3 -->|"volatile写 hb<br/>后续的volatile读"| E3[volatile可见性]
style HB fill:#FFD700,stroke:#333,stroke-width:2px
| 规则 | 说明 |
|---|---|
| 程序顺序规则 | 一个线程中每个操作happens-before该线程中后续操作 |
| 监视器锁规则 | 对一个锁的unlock操作happens-before后续对同一个锁的lock操作 |
| volatile变量规则 | 对一个volatile变量的写操作happens-before后续对该变量的读操作 |
| 线程启动规则 | Thread.start() happens-before 该线程中的任何操作 |
| 线程终止规则 | 线程中的所有操作happens-before其他线程检测到该线程终止(join/isAlive) |
| 线程中断规则 | 对线程interrupt()的调用happens-before被中断线程检测到中断事件 |
| 对象终结规则 | 对象的构造函数执行完毕happens-before finalize()方法 |
| 传递性 | 如果A happens-before B,B happens-before C,则A happens-before C |
4.2 happens-before与实际执行顺序
需要注意的是,happens-before并不意味着实际的执行顺序。JMM允许在不影响happens-before语义的情况下进行重排序:
1 | |
5. volatile的内存语义
5.1 volatile的两大特性
volatile关键字提供了两个语义保证:
- 可见性:对volatile变量的写操作会立即刷新到主内存,读操作会从主内存重新读取
- 有序性:禁止volatile变量相关的指令重排序
1 | |
5.2 volatile的内存屏障
JMM通过插入内存屏障(Memory Barrier)来实现volatile的语义:
graph TB
subgraph volatile写操作
SW1[StoreStore屏障] --> VW[volatile写]
VW --> SW2[StoreLoad屏障]
end
subgraph volatile读操作
VR[volatile读] --> SR1[LoadLoad屏障]
SR1 --> SR2[LoadStore屏障]
end
style VW fill:#FF6347
style VR fill:#4169E1,color:#fff
style SW1 fill:#FFD700
style SW2 fill:#FFD700
style SR1 fill:#FFD700
style SR2 fill:#FFD700
| 屏障类型 | 指令示例 | 说明 |
|---|---|---|
| LoadLoad | Load1; LoadLoad; Load2 | 确保Load1在Load2之前执行 |
| StoreStore | Store1; StoreStore; Store2 | 确保Store1在Store2之前刷新到主内存 |
| LoadStore | Load1; LoadStore; Store2 | 确保Load1在Store2之前执行 |
| StoreLoad | Store1; StoreLoad; Load2 | 确保Store1刷新到主内存后才执行Load2,开销最大 |
5.3 volatile不保证原子性
1 | |
6. 双重检查锁定(DCL)与volatile
双重检查锁定是volatile最经典的应用场景之一:
1 | |
为什么必须用volatile?因为instance = new Singleton()不是原子操作,它实际上包含三个步骤:
graph LR
S1["1. 分配内存空间"] --> S2["2. 初始化对象"]
S2 --> S3["3. 将引用指向内存地址"]
S1b["1. 分配内存空间"] --> S3b["2. 将引用指向内存地址<br/>(对象尚未初始化!)"]
S3b --> S2b["3. 初始化对象"]
subgraph 正常顺序
S1
S2
S3
end
subgraph 重排序后
S1b
S3b
S2b
end
style S3b fill:#FF6347,color:#fff
如果发生重排序,步骤2和3交换,另一个线程可能在第一次检查时看到instance != null,但获取到的是一个尚未完成初始化的对象,导致程序出错。volatile通过禁止重排序解决了这个问题。
7. final字段的内存语义
JMM对final字段也有特殊的内存语义保证:
1 | |
this引用逸出的反面示例
1 | |
8. 最佳实践
- 正确使用volatile:仅适用于一写多读的简单标志位场景,不能替代锁来保护复合操作。
- 理解happens-before:这是分析并发正确性的根本工具,遇到并发问题时先从happens-before关系入手。
- 不要依赖指令执行顺序:除非有happens-before关系保证,否则不要假设任何执行顺序。
- 安全发布对象:使用volatile、synchronized、final或并发容器来安全地发布共享对象。
- 避免构造函数中的this逸出:否则final字段的语义保证将失效。
- 使用高层并发工具:优先使用
java.util.concurrent包提供的线程安全容器和同步工具,而非自己实现底层同步。
9. 总结
Java内存模型是连接Java语言层面并发语义和底层硬件内存架构的桥梁。它通过以下核心机制保证了多线程程序的正确性:
- happens-before规则定义了操作之间的可见性保证,是并发正确性的理论基础
- volatile通过内存屏障实现了可见性和有序性,是轻量级的同步手段
- synchronized通过Monitor机制提供了原子性、可见性和有序性的完整保证
- final通过特殊的内存语义保证了不可变对象的安全发布
理解JMM不仅有助于编写正确的并发程序,更能帮助我们理解各种并发工具和设计模式的底层原理,是成为高级Java开发者的必经之路。