Java内存模型(JMM)深度解析
JMM规范与内存屏障
1. JMM的核心目标
Java内存模型(JMM)的核心目标是解决由于现代计算机硬件架构导致的多线程并发问题,主要包括:
graph TD
A[JMM核心目标] --> B[解决可见性问题]
A --> C[解决有序性问题]
A --> D[保证原子性]
A --> E[屏蔽硬件差异]
B --> F[确保线程间数据同步]
C --> G[控制指令重排序]
D --> H[提供原子操作保证]
E --> I[跨平台一致性]
style A fill:#ff9999
style B fill:#99ccff
style C fill:#99ccff
style D fill:#99ccff
style E fill:#99ccff
2. JMM的规范
2.1 JMM的核心概念
JMM(Java内存模型Java Memory Model,简称JMM)本身是一种抽象的概念并不真实存在,它仅仅描述的是一组约定或规范,通过这组规范定义了程序中(尤其是多线程)各个变量的读写访问方式并决定一个线程对共享变量的写入何时以及如何变成对另一个线程可见,关键技术点都是围绕多线程的原子性、可见性和有序性展开的。
JMM的关键作用:
- 通过JMM来实现线程和主内存之间的抽象关系
- 屏蔽各个硬件平台和操作系统的内存访问差异以实现让Java程序在各种平台下都能达到一致的内存访问效果
2.2 JMM的抽象结构
graph TD
subgraph 线程1
T1[线程1操作]
WM1[工作内存1]
T1 --> WM1
end
subgraph 线程2
T2[线程2操作]
WM2[工作内存2]
T2 --> WM2
end
subgraph 线程N
TN[线程N操作]
WMN[工作内存N]
TN --> WMN
end
MM[主内存]
WM1 <-..-> MM
WM2 <-..-> MM
WMN <-..-> MM
%% 样式定义
classDef thread fill:#99ccff,stroke:#333,stroke-width:1px;
classDef main fill:#ff9999,stroke:#333,stroke-width:2px;
class T1,T2,TN thread;
class WM1,WM2,WMN thread;
class MM main;
JMM抽象模型的核心组件:
graph TD
A[JMM抽象模型] --> B[主内存]
A --> C[工作内存]
B --> B1[存储所有共享变量]
B --> B2[线程间通信的唯一途径]
B --> B3[对应物理内存RAM]
C --> C1[每个线程私有]
C --> C2[存储共享变量的副本]
C --> C3[抽象概念包含]
C3 --> C31[CPU寄存器]
C3 --> C32[各级缓存L1L2L3]
C3 --> C33[StoreBuffer]
C3 --> C34[编译器优化临时存储]
style A fill:#ff9999
style B fill:#ffcc99
style C fill:#99ccff
2.3 JMM的8种原子操作
基于前面介绍的JMM抽象结构,我们需要了解变量在主内存和工作内存之间是如何交互的。JMM定义了8种原子操作来精确描述这种交互过程:
flowchart TD
subgraph "CPU执行引擎"
CPU["执行引擎<br/>(CPU核心)"]
end
subgraph "工作内存区域"
REG["CPU寄存器<br/>(临时存储)"]
L1["L1缓存<br/>(最快访问)"]
L2["L2缓存<br/>(次快访问)"]
BUFFER["Store Buffer<br/>(写缓冲区)"]
end
subgraph "主内存区域"
RAM["主内存RAM<br/>(共享存储)"]
end
subgraph "8种原子操作详解"
READ["read<br/>从主内存读取变量值<br/>到工作内存传输通道"]
LOAD["load<br/>将read的值载入<br/>工作内存变量副本"]
USE["use<br/>从工作内存传递值<br/>给CPU执行引擎"]
ASSIGN["assign<br/>从CPU执行引擎<br/>接收值到工作内存"]
STORE["store<br/>将工作内存变量值<br/>传送到主内存通道"]
WRITE["write<br/>将store的值<br/>写入主内存变量"]
LOCK["lock<br/>标记主内存变量<br/>为线程独占状态"]
UNLOCK["unlock<br/>释放主内存变量<br/>的锁定状态"]
end
%% 数据流向
RAM -.->|"1.read操作"| READ
READ -.->|"2.load操作"| LOAD
LOAD -.->|"传输到"| L1
L1 -.->|"3.use操作"| USE
USE -.->|"传递给"| CPU
CPU -.->|"4.assign操作"| ASSIGN
ASSIGN -.->|"存储到"| L2
L2 -.->|"5.store操作"| STORE
STORE -.->|"6.write操作"| WRITE
WRITE -.->|"写回"| RAM
%% 锁操作
RAM -.->|"7.lock操作"| LOCK
UNLOCK -.->|"8.unlock操作"| RAM
%% 样式设置
style CPU fill:#ff6b6b
style RAM fill:#4ecdc4
style REG fill:#45b7d1
style L1 fill:#96ceb4
style L2 fill:#feca57
style BUFFER fill:#ff9ff3
style READ fill:#ff9999
style WRITE fill:#ff9999
style LOAD fill:#99ccff
style STORE fill:#99ccff
style USE fill:#99ff99
style ASSIGN fill:#99ff99
style LOCK fill:#ffcc99
style UNLOCK fill:#ffcc99
8种原子操作的详细说明:
操作 | 作用域 | 具体操作位置 | 详细描述 | 硬件对应 |
---|---|---|---|---|
read | 主内存→传输通道 | 主内存RAM | 从主内存中读取变量值,准备传输到工作内存 | 内存控制器读取操作 |
load | 传输通道→工作内存 | CPU缓存(L1/L2/L3) | 将read操作获取的值载入工作内存的变量副本中 | 缓存行填充(Cache Line Fill) |
use | 工作内存→CPU | CPU寄存器 | 将工作内存中的变量值传递给CPU执行引擎使用 | 寄存器加载指令(MOV) |
assign | CPU→工作内存 | CPU寄存器→缓存 | 将CPU执行引擎计算的结果赋值给工作内存变量 | 寄存器存储指令(MOV) |
store | 工作内存→传输通道 | Store Buffer/写缓冲区 | 将工作内存中的变量值传送到主内存写入通道 | 写缓冲区排队 |
write | 传输通道→主内存 | 主内存RAM | 将store操作的值最终写入主内存的变量中 | 内存控制器写入操作 |
lock | 主内存 | 内存地址+锁标志位 | 将主内存中的变量标记为当前线程独占状态 | 原子CAS操作/总线锁 |
unlock | 主内存 | 内存地址+锁标志位 | 释放主内存变量的锁定状态,允许其他线程访问 | 原子写操作/内存屏障 |
操作流程说明:
- 读取流程:read → load → use(主内存 → 工作内存 → CPU寄存器)
- 写入流程:assign → store → write(CPU寄存器 → 工作内存 → 主内存)
- 同步流程:lock → (读写操作) → unlock(确保原子性和可见性)
2.4 JMM的操作规则
理解了8种原子操作的具体实现后,我们需要掌握JMM对这些操作制定的严格规则。这些规则确保了多线程环境下的数据一致性和可见性:
基本规则:
- read和load必须成对出现:不允许单独的read或load操作
- store和write必须成对出现:不允许单独的store或write操作
- 不允许丢弃assign操作:工作内存中变量改变后必须同步回主内存
- 不允许无原因同步:没有assign操作就不能从工作内存同步回主内存
- 新变量只能在主内存中诞生:工作内存中不能直接使用未初始化的变量
锁操作规则:
- lock和unlock必须成对出现
- lock操作会清空工作内存:重新从主内存加载变量
- unlock前必须先store+write:将变量同步回主内存
3. JMM的三大特性
在深入了解了JMM的规范和8种原子操作后,我们来探讨JMM如何通过这些机制保证多线程编程的三大核心特性。这三大特性是JMM设计的根本目标,也是我们编写正确并发程序的理论基础。
3.1 原子性(Atomicity)
定义:一个操作是不可中断的,要么全部执行成功,要么全部不执行。
JMM保证的原子性操作:
- 基本类型的读写操作(long和double的非volatile变量除外)
- 所有引用类型的读写操作
- volatile修饰的所有变量的读写操作
- 锁操作(monitorenter和monitorexit指令)
实现机制:
flowchart LR
A[原子性保证] --> B[JVM内部实现]
A --> C[synchronized]
A --> D[Lock接口实现类]
A --> E[原子类]
B --> F[字节码原子指令]
C --> G[监视器Monitor]
D --> H[AQS框架]
E --> I[CAS操作]
style A fill:#ff9999
style B fill:#99ccff
style C fill:#99ccff
style D fill:#99ccff
style E fill:#99ccff
3.2 可见性(Visibility)
定义:当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。
硬件层面的可见性问题:
sequenceDiagram
participant T1 as 线程1
participant C1 as CPU1缓存
participant MM as 主内存
participant C2 as CPU2缓存
participant T2 as 线程2
T1->>C1: 写入X=1
Note over C1: 缓存更新
C1-->>MM: 延迟写回
T2->>C2: 读取X
Note over C2: 缓存中X仍为0
C2->>T2: 返回X=0
Note over T2: 可见性问题!
JMM提供的可见性保证:
- volatile变量的读写
- synchronized的解锁操作(退出同步块)
- final字段的初始化
- Thread.start()的调用
- Thread.join()的成功返回
- 传递性规则保证的可见性
3.3 有序性(Ordering)
定义:程序执行的顺序按照代码的先后顺序执行。
重排序类型:
graph TD
A[重排序类型] --> B[编译器优化重排序]
A --> C[指令级并行重排序]
A --> D[内存系统重排序]
B --> E[JIT编译器]
C --> F[CPU流水线]
D --> G[Store Buffer/缓存]
style A fill:#ff9999
style B fill:#99ccff
style C fill:#99ccff
style D fill:#99ccff
重排序对多线程的影响:
// 初始状态: x = 0, y = 0
// 线程1
void thread1() {
x = 1; // A
r1 = y; // B
}
// 线程2
void thread2() {
y = 1; // C
r2 = x; // D
}
// 可能的结果: r1 = 0, r2 = 0(违反直觉)
重排序的限制:
- as-if-serial语义(单线程程序的结果不变)
- volatile变量的禁止重排序
- synchronized的锁语义
- happens-before规则
4. Happens-Before原则详解
通过前面对JMM规范和三大特性的学习,我们了解了JMM的基本机制。但在实际编程中,我们需要一个更高层次的抽象来判断多线程程序的正确性。这就是Happens-Before原则——它是JMM提供给程序员的最重要的工具,让我们能够在不深入底层实现的情况下,准确判断并发程序的行为。
4.1 Happens-Before的定义与重要性
Happens-Before原则是JMM中最重要的概念之一,它定义了内存操作之间的偏序关系,用于保证内存可见性。
核心含义:
- 如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见
- 第一个操作的执行顺序排在第二个操作之前
- 两个操作之间存在happens-before关系,并不意味着一定要按照happens-before原则制定的顺序来执行
- 如果重排序之后的执行结果与按照happens-before关系来执行的结果一致,那么这种重排序并不非法
重要性:
- 它是判断数据是否存在竞争、线程是否安全的非常有用的手段
- 依赖这个原则,我们可以通过几条简单规则解决并发环境下两个操作之间是否可能存在冲突的所有问题
- 避免陷入Java内存模型苦涩难懂的底层编译原理之中
4.2 Happens-Before的8条规则详解
4.2.1 程序顺序规则(Program Order Rule)
定义:在一个线程内,按照代码顺序,写在前面的操作先行发生于写在后面的操作。
核心要点:
- 这是单线程内的顺序保证
- 编译器和处理器可以重排序,但必须保证单线程的执行结果不变(as-if-serial语义)
- 只对控制依赖和数据依赖的操作有约束
示例代码:
public class ProgramOrderExample {
private int a = 0;
private int b = 0;
public void method() {
a = 1; // 操作1
b = 2; // 操作2
int c = a + b; // 操作3
// 根据程序顺序规则:
// 操作1 happens-before 操作2
// 操作2 happens-before 操作3
// 但操作1和操作2可能被重排序(无数据依赖)
// 操作3不能重排序到操作1、2之前(有数据依赖)
}
}
4.2.2 监视器锁规则(Monitor Lock Rule)
定义:一个unlock操作先行发生于后面(时间上的先后)对同一个锁的lock操作。
核心要点:
- 前面的锁必须解锁了,后面才能继续加锁
- 保证了临界区的互斥性和可见性
- synchronized关键字的底层实现基础
示例代码:
public class MonitorLockExample {
private int sharedData = 0;
private final Object lock = new Object();
// 线程1
public void writer() {
synchronized (lock) { // 获取锁
sharedData = 42; // 写操作
} // 释放锁 - 这个unlock操作
}
// 线程2
public void reader() {
synchronized (lock) { // 获取锁 - happens-before于上面的unlock
int value = sharedData; // 能看到sharedData = 42
System.out.println(value); // 输出42
}
}
}
时序图:
sequenceDiagram
participant T1 as 线程1
participant Lock as 监视器锁
participant T2 as 线程2
participant Memory as 共享内存
T1->>Lock: 获取锁
T1->>Memory: 写入sharedData=42
T1->>Lock: 释放锁 (unlock)
Note over Lock: happens-before关系建立
T2->>Lock: 获取锁 (lock)
T2->>Memory: 读取sharedData
Memory->>T2: 返回42(可见)
T2->>Lock: 释放锁
4.2.3 volatile变量规则(Volatile Variable Rule)
定义:对一个volatile变量的写操作先行发生于后面对这个变量的读操作。
核心要点:
- 保证volatile变量的可见性
- 禁止volatile变量与普通变量的重排序
- 通过内存屏障实现
示例代码:
public class VolatileExample {
private int normalVar = 0;
private volatile boolean flag = false;
// 线程1:写操作
public void writer() {
normalVar = 42; // 操作1
flag = true; // 操作2(volatile写)
// 操作1 happens-before 操作2(程序顺序规则)
}
// 线程2:读操作
public void reader() {
if (flag) { // 操作3(volatile读)
int value = normalVar; // 操作4
System.out.println(value); // 输出42
}
// 操作2 happens-before 操作3(volatile规则)
// 操作3 happens-before 操作4(程序顺序规则)
// 传递性:操作1 happens-before 操作4
}
}
4.2.4 线程启动规则(Thread Start Rule)
定义:Thread对象的start()方法先行发生于此线程的每一个动作。
核心要点:
- 主线程中对变量的修改对新启动的线程可见
- start()方法的调用建立happens-before关系
示例代码:
public class ThreadStartExample {
private int sharedData = 0;
public void startThreadExample() {
sharedData = 42; // 操作1
Thread thread = new Thread(() -> {
// 操作2:能够看到sharedData = 42
System.out.println("sharedData = " + sharedData);
});
thread.start(); // start()调用 happens-before 线程内的所有操作
// 操作1 happens-before start()调用(程序顺序规则)
// start()调用 happens-before 操作2(线程启动规则)
// 传递性:操作1 happens-before 操作2
}
}
4.2.5 线程终止规则(Thread Termination Rule)
定义:线程中的所有操作都先行发生于对此线程的终止检测。
核心要点:
- 可以通过Thread.join()、Thread.isAlive()等方法检测线程终止
- 线程内的所有操作对检测到终止的线程可见
示例代码:
public class ThreadTerminationExample {
private int result = 0;
public void threadTerminationExample() throws InterruptedException {
Thread worker = new Thread(() -> {
// 线程内的操作
for (int i = 0; i < 1000; i++) {
result += i; // 操作1
}
// 所有操作完成
});
worker.start();
worker.join(); // 等待线程终止
// join()返回后,能够看到result的最终值
System.out.println("Final result: " + result); // 操作2
// 操作1 happens-before 操作2(线程终止规则)
}
}
4.2.6 线程中断规则(Thread Interruption Rule)
定义:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生。
核心要点:
- 必须先调用interrupt()方法设置中断标志位
- 然后才能通过Thread.interrupted()或isInterrupted()检测到中断
示例代码:
public class ThreadInterruptionExample {
public void interruptionExample() {
Thread worker = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
// 执行工作
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// 检测到中断 - 操作2
System.out.println("Thread interrupted!");
Thread.currentThread().interrupt(); // 重新设置中断标志
break;
}
}
});
worker.start();
// 主线程中断工作线程
worker.interrupt(); // 操作1
// 操作1 happens-before 操作2(线程中断规则)
}
}
4.2.7 对象终结规则(Finalizer Rule)
定义:一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。
核心要点:
- 对象没有完成初始化之前,不能调用finalize()方法
- 构造函数中的所有操作对finalize()方法可见
示例代码:
public class FinalizerExample {
private int value;
public FinalizerExample(int value) {
this.value = value; // 操作1:初始化
} // 构造函数结束
@Override
protected void finalize() throws Throwable {
// 操作2:能够看到value的值
System.out.println("Finalizing object with value: " + value);
super.finalize();
// 操作1 happens-before 操作2(对象终结规则)
}
}
4.2.8 传递性规则(Transitivity Rule)
定义:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C。
核心要点:
- 这是happens-before关系的传递性
- 允许我们建立复杂的可见性关系链
- 是其他规则组合使用的基础
示例代码:
public class TransitivityExample {
private int data = 0;
private volatile boolean step1Complete = false;
private volatile boolean step2Complete = false;
// 线程1
public void thread1() {
data = 42; // 操作A
step1Complete = true; // 操作B(volatile写)
}
// 线程2
public void thread2() {
while (!step1Complete) { // 操作C(volatile读)
Thread.yield();
}
// 这里可以安全地读取data
int value = data; // 操作D
step2Complete = true; // 操作E(volatile写)
}
// 线程3
public void thread3() {
while (!step2Complete) { // 操作F(volatile读)
Thread.yield();
}
// 这里也可以安全地读取data
int value = data; // 操作G
System.out.println(value); // 输出42
}
/*
* Happens-before关系链:
* A happens-before B (程序顺序规则)
* B happens-before C (volatile规则)
* C happens-before D (程序顺序规则)
* D happens-before E (程序顺序规则)
* E happens-before F (volatile规则)
* F happens-before G (程序顺序规则)
*
* 传递性:A happens-before G
* 因此操作G能够看到操作A的结果
*/
}
5. 内存屏障机制与Happens-Before原则的协调
内存屏障(Memory Barrier/Memory Fence)是JMM在硬件层面实现happens-before原则的核心机制。它是一种特殊的CPU指令,用于控制内存操作的顺序和可见性,确保多线程程序的正确性。
5.1 内存屏障的分类与作用
graph TD
A[内存屏障分类] --> B[按操作类型分类]
A --> C[按强度分类]
B --> D[LoadLoad屏障]
B --> E[StoreStore屏障]
B --> F[LoadStore屏障]
B --> G[StoreLoad屏障]
C --> H[编译器屏障]
C --> I[CPU屏障]
C --> J[全屏障]
D --> D1["读-读屏障<br/>防止读操作重排序"]
E --> E1["写-写屏障<br/>防止写操作重排序"]
F --> F1["读-写屏障<br/>防止读写重排序"]
G --> G1["写-读屏障<br/>最强屏障,开销最大"]
style A fill:#ff9999
style B fill:#99ccff
style C fill:#99ccff
四种基本内存屏障详解:
屏障类型 | 作用机制 | 硬件实现 | 性能开销 | 应用场景 |
---|---|---|---|---|
LoadLoad | 确保Load1的数据装载先于Load2及后续装载指令 | CPU缓存一致性协议 | 低 | volatile读后的普通读 |
StoreStore | 确保Store1的数据对其他处理器可见先于Store2 | 写缓冲区刷新 | 中 | volatile写前的普通写 |
LoadStore | 确保Load1的数据装载先于Store2及后续存储指令 | 防止读写重排序 | 中 | volatile读后的普通写 |
StoreLoad | 确保Store1的数据对其他处理器可见先于Load2 | 全屏障,最强约束 | 高 | volatile写后的读操作 |
5.2 内存屏障与JVM 8大原子操作的协调
内存屏障通过与JVM的8种原子操作协调工作,确保happens-before原则在硬件层面的正确实现:
flowchart TD
subgraph "JVM 8大原子操作层"
READ["read<br/>主内存读取"]
LOAD["load<br/>载入工作内存"]
USE["use<br/>传递给执行引擎"]
ASSIGN["assign<br/>执行引擎赋值"]
STORE["store<br/>传送到主内存"]
WRITE["write<br/>写入主内存"]
LOCK["lock<br/>锁定变量"]
UNLOCK["unlock<br/>释放锁定"]
end
subgraph "内存屏障层"
LL["LoadLoad屏障"]
SS["StoreStore屏障"]
LS["LoadStore屏障"]
SL["StoreLoad屏障"]
end
subgraph "硬件实现层"
CACHE["CPU缓存一致性"]
BUFFER["Store Buffer管理"]
PIPELINE["CPU流水线控制"]
BUS["总线锁定"]
end
%% 操作间的屏障插入
READ -.->|"插入LoadLoad"| LL
LOAD -.->|"插入LoadStore"| LS
STORE -.->|"插入StoreStore"| SS
WRITE -.->|"插入StoreLoad"| SL
%% 屏障到硬件的映射
LL --> CACHE
SS --> BUFFER
LS --> PIPELINE
SL --> BUS
%% 锁操作的特殊处理
LOCK -.->|"全屏障"| SL
UNLOCK -.->|"全屏障"| SL
style READ fill:#e1f5fe
style WRITE fill:#e1f5fe
style LL fill:#f3e5f5
style SL fill:#ffebee
协调机制详解:
内存屏障与JVM 8大原子操作的协调是一个精密的过程,需要在正确的时机插入合适的屏障来保证内存一致性。以下是四种基本内存屏障的详细协调机制:
5.2.1 LoadLoad屏障的协调机制
作用:确保屏障前的读操作完成后,才能执行屏障后的读操作。
详细协调过程:
public class LoadLoadBarrierExample {
private int data1 = 0;
private int data2 = 0;
private volatile boolean flag = false;
public void readerThread() {
if (flag) { // volatile读操作
// JVM自动插入LoadLoad屏障
// 确保volatile读先于后续普通读
int value1 = data1; // 普通读操作1
int value2 = data2; // 普通读操作2
// 协调过程详解:
// 1. read(flag) - 从主内存读取flag
// 2. load(flag) - 载入工作内存
// 3. use(flag) - 传递给CPU判断
// 4. 插入LoadLoad屏障 - 确保flag读取完成
// 5. read(data1) - 读取data1(保证在flag之后)
// 6. load(data1) - 载入data1
// 7. use(data1) - 使用data1
// 8. read(data2) - 读取data2
// 9. load(data2) - 载入data2
// 10. use(data2) - 使用data2
System.out.println("data1: " + value1 + ", data2: " + value2);
}
}
}
x86平台硬件实现:
- 由于x86采用强内存模型(TSO - Total Store Ordering),LoadLoad操作天然保证顺序性
- 处理器内部通过流水线和乱序执行优化,但对程序员透明
- 无需插入额外的硬件指令,性能开销最小
5.2.2 StoreStore屏障的协调机制
作用:确保屏障前的写操作对其他处理器可见后,才能执行屏障后的写操作。
详细协调过程:
public class StoreStoreBarrierExample {
private int data1 = 0;
private int data2 = 0;
private volatile boolean ready = false;
public void writerThread() {
data1 = 100; // 普通写操作1
data2 = 200; // 普通写操作2
// JVM自动插入StoreStore屏障
// 确保普通写先于volatile写对其他线程可见
ready = true; // volatile写操作
// 协调过程详解:
// 1. assign(data1, 100) - CPU将100赋值给data1的工作内存副本(L1缓存)
// 2. store(data1) - 将data1从L1缓存传送到Store Buffer
// 3. write(data1) - 从Store Buffer写入主内存,同时:
// - 通过总线发送Invalidate消息使其他CPU的data1缓存行失效
// - 等待其他CPU返回Invalidate Acknowledge确认
// - 将data1标记为Modified状态(MESI协议)
// 4. assign(data2, 200) - CPU将200赋值给data2的工作内存副本(L1缓存)
// 5. store(data2) - 将data2从L1缓存传送到Store Buffer
// 6. write(data2) - 从Store Buffer写入主内存,执行相同的缓存一致性协议
// 7. 插入StoreStore屏障 - x86 TSO模型确保:
// - Store Buffer中的所有写操作按程序顺序排队
// - 前面的写操作必须完全可见后,才能执行后续写操作
// - 硬件保证写操作的全局可见性顺序
// 8. assign(ready, true) - CPU将true赋值给ready的工作内存副本
// 9. store(ready) - 将ready传送到Store Buffer(排在data1、data2之后)
// 10. write(ready) - 写入主内存,保证在data1、data2全局可见之后
}
}
x86平台硬件实现:
- x86的TSO内存模型保证写操作的顺序性,StoreStore天然满足
- 写操作通过Store Buffer进行优化,但保持程序顺序
- 处理器确保所有写操作按程序顺序对其他处理器可见
5.2.3 LoadStore屏障的协调机制
作用:确保屏障前的读操作完成后,才能执行屏障后的写操作。
详细协调过程:
public class LoadStoreBarrierExample {
private int sharedData = 0;
private volatile boolean flag = false;
public void processorThread() {
if (flag) { // volatile读操作
// JVM自动插入LoadStore屏障
// 确保volatile读先于后续写操作
sharedData = 42; // 普通写操作
// 协调过程详解:
// 1. read(flag) - 从主内存读取flag值
// 2. load(flag) - 将flag载入工作内存
// 3. use(flag) - 传递给CPU进行条件判断
// 4. 插入LoadStore屏障 - 确保flag读取完成
// 5. assign(sharedData, 42) - CPU将42赋值给sharedData工作内存
// 6. store(sharedData) - 传送sharedData到主内存通道
// 7. write(sharedData) - 写入主内存(保证在flag读取之后)
System.out.println("Data processed: " + sharedData);
}
}
}
x86平台硬件实现:
- x86强内存模型确保读操作不会越过写操作
- 处理器的Load-Store单元维护操作顺序
- 通过内存依赖检测硬件自动保证LoadStore顺序
5.2.4 StoreLoad屏障的协调机制
作用:确保屏障前的写操作对其他处理器可见后,才能执行屏障后的读操作。这是最强的屏障,开销最大。
详细协调过程:
public class StoreLoadBarrierExample {
private volatile int counter = 0;
private int localCache = 0;
public void incrementAndRead() {
counter++; // volatile写操作
// JVM自动插入StoreLoad屏障
// 确保counter写入对所有处理器可见后,才能进行后续读操作
int currentValue = counter; // volatile读操作
localCache = currentValue; // 普通写操作
// 协调过程详解:
// 写操作阶段:
// 1. read(counter) - 读取counter当前值
// 2. load(counter) - 载入工作内存
// 3. use(counter) - 传递给CPU
// 4. assign(counter, value+1) - CPU计算并赋值新值
// 5. store(counter) - 传送到主内存通道
// 6. write(counter) - 写入主内存
//
// 插入StoreLoad屏障:
// 7. 刷新Store Buffer - 确保所有写操作立即可见
// 8. 失效本地缓存 - 确保后续读取最新值
//
// 读操作阶段:
// 9. read(counter) - 从主内存重新读取(保证是最新值)
// 10. load(counter) - 载入工作内存
// 11. use(counter) - 传递给CPU
// 12. assign(localCache, value) - 赋值给localCache
System.out.println("Counter: " + currentValue + ", Cache: " + localCache);
}
}
x86平台硬件实现:
- 使用
mfence
指令实现全内存屏障,确保所有内存操作的全局可见性 - 或使用
lock
前缀指令(如lock addl $0,0(%%esp)
)作为替代 mfence
会刷新Store Buffer并等待所有pending的内存操作完成- 这是开销最大的屏障,但提供最强的内存一致性保证
5.2.5 锁操作的全屏障协调
synchronized的完整屏障协调:
public class SynchronizedFullBarrierExample {
private int sharedData = 0;
private final Object lock = new Object();
public void synchronizedOperation() {
synchronized (lock) { // monitorenter指令
// 进入同步块时的屏障协调:
// 1. lock(lock对象) - 获取监视器锁
// 2. 插入LoadLoad屏障 - 确保锁获取先于临界区读操作
// 3. 插入LoadStore屏障 - 确保锁获取先于临界区写操作
int oldValue = sharedData; // 临界区读操作
sharedData = oldValue + 1; // 临界区写操作
// 退出同步块时的屏障协调:
// 4. 插入StoreStore屏障 - 确保临界区写先于锁释放
// 5. 插入StoreLoad屏障 - 确保临界区写对后续线程可见
// 6. unlock(lock对象) - 释放监视器锁
} // monitorexit指令
// 详细的JVM原子操作序列:
// 锁获取阶段:
// - lock(lock对象)
// - 全屏障插入
//
// 临界区操作:
// - read(sharedData) -> load(sharedData) -> use(sharedData)
// - assign(sharedData, newValue) -> store(sharedData) -> write(sharedData)
//
// 锁释放阶段:
// - 全屏障插入
// - unlock(lock对象)
}
}
5.3 volatile变量的内存屏障实现
volatile变量是内存屏障机制最典型的应用,它通过精确的屏障插入策略实现happens-before语义:
sequenceDiagram
participant T1 as 线程1
participant WM1 as 工作内存1
participant MB as 内存屏障
participant MM as 主内存
participant WM2 as 工作内存2
participant T2 as 线程2
Note over T1,T2: volatile写操作的屏障序列
T1->>WM1: 普通写操作
WM1->>MB: StoreStore屏障
Note over MB: 确保普通写先于volatile写
WM1->>MM: volatile写操作
MM->>MB: StoreLoad屏障
Note over MB: 确保volatile写对后续读可见
Note over T1,T2: volatile读操作的屏障序列
T2->>MM: volatile读操作
MM->>MB: LoadLoad屏障
Note over MB: 确保volatile读先于后续读
MM->>WM2: 后续读操作
WM2->>MB: LoadStore屏障
Note over MB: 确保volatile读先于后续写
WM2->>T2: 传递给线程2
volatile的完整屏障模式:
public class VolatileBarrierExample {
private int normalVar1 = 0;
private int normalVar2 = 0;
private volatile boolean flag = false;
// 写线程的屏障插入
public void writer() {
normalVar1 = 1; // 普通写1
normalVar2 = 2; // 普通写2
// 编译器插入StoreStore屏障
flag = true; // volatile写
// 编译器插入StoreLoad屏障
}
// 读线程的屏障插入
public void reader() {
if (flag) { // volatile读
// 编译器插入LoadLoad屏障
// 编译器插入LoadStore屏障
int a = normalVar1; // 普通读1
int b = normalVar2; // 普通读2
System.out.println(a + ", " + b); // 输出: 1, 2
}
}
}
5.4 synchronized的内存屏障实现
synchronized关键字通过monitorenter和monitorexit指令配合内存屏障实现happens-before语义:
public class SynchronizedBarrierExample {
private int sharedData = 0;
private final Object lock = new Object();
public void synchronizedMethod() {
synchronized (lock) { // monitorenter指令
// JVM插入LoadLoad + LoadStore屏障
// 确保锁内的读写不会重排序到锁外
sharedData = 42; // 临界区操作
int temp = sharedData;
// JVM插入StoreStore + StoreLoad屏障
} // monitorexit指令
// 确保锁内的写操作对后续线程可见
}
}
synchronized的屏障策略:
- 进入同步块:插入LoadLoad + LoadStore屏障
- 退出同步块:插入StoreStore + StoreLoad屏障
- 效果:确保临界区内的操作不会与锁操作重排序
5.5 x86平台的内存屏障实现
x86平台采用TSO(Total Store Ordering)强内存模型,为内存屏障提供了高效的硬件支持:
x86平台的内存屏障指令:
- LoadLoad/LoadStore/StoreStore:由于TSO模型的强一致性保证,这些屏障通常不需要显式的硬件指令
- StoreLoad:使用
mfence
指令或带lock
前缀的指令实现全内存屏障
x86内存屏障的具体实现:
; LoadLoad屏障 - 通常为空操作(NOP)
; x86 TSO模型天然保证
; StoreStore屏障 - 通常为空操作(NOP)
; x86写操作天然有序
; LoadStore屏障 - 通常为空操作(NOP)
; x86强内存模型保证
; StoreLoad屏障 - 需要显式指令
mfence ; 全内存屏障
; 或者
lock addl $0,0(%%esp) ; 使用lock前缀的替代方案
x86内存屏障的性能特点:
graph LR
A[x86内存屏障性能] --> B[LoadLoad - 零开销]
A --> C[LoadStore - 零开销]
A --> D[StoreStore - 零开销]
A --> E[StoreLoad - 高开销]
style E fill:#ff9999
style B fill:#99ff99
style C fill:#99ff99
style D fill:#99ff99
TSO模型的优势:
- 大多数内存屏障操作无需额外指令,性能优异
- 只有StoreLoad屏障需要显式的
mfence
指令 - 硬件自动维护内存一致性,简化了JVM实现
x86平台的内存屏障实现示例:
// x86平台的内存屏障实现(简化)
public class X86MemoryBarrier {
// x86的强内存模型特性
// LoadLoad, StoreStore, LoadStore 天然保证,无需额外指令
// LoadLoad屏障:x86 TSO模型天然保证,无需额外操作
public static void loadLoadBarrier() {
// 编译为NOP指令或直接省略
}
// StoreStore屏障:x86写操作天然有序,无需额外操作
public static void storeStoreBarrier() {
// 编译为NOP指令或直接省略
}
// LoadStore屏障:x86强内存模型保证,无需额外操作
public static void loadStoreBarrier() {
// 编译为NOP指令或直接省略
}
// StoreLoad屏障:需要显式的mfence指令
public static void storeLoadBarrier() {
// 只有StoreLoad需要显式屏障
// 在x86平台编译为mfence指令
Unsafe.getUnsafe().fullFence();
}
// volatile写的x86实现
public static void volatileWrite(int value, int[] array, int index) {
array[index] = value; // 普通写操作
storeLoadBarrier(); // 插入StoreLoad屏障
}
// volatile读的x86实现
public static int volatileRead(int[] array, int index) {
int value = array[index]; // 普通读操作
// LoadLoad和LoadStore屏障在x86上为空操作
return value;
}
}
通过内存屏障机制与JVM 8大原子操作的精密协调,JMM成功地将抽象的happens-before原则转化为具体的硬件指令,确保了Java程序在各种硬件平台上的一致性和正确性。这种分层设计既保证了理论的严谨性,又实现了实践的高效性。
总结
本文深入探讨了Java内存模型(JMM)的核心机制,从JMM的规范和8种原子操作开始,详细阐述了JMM的三大特性(原子性、可见性、有序性),深入分析了Happens-Before原则的8条规则,最后揭示了内存屏障如何与JVM原子操作协调实现happens-before语义。