Java内存模型(JMM)
Java内存模型(Java Memory Model, JMM)是一个抽象的概念,它定义了线程和主内存之间的抽象关系,以及多线程环境下共享变量的读写规则。理解JMM对于编写正确的并发程序至关重要。
为什么需要JMM?🤔
硬件内存架构的复杂性:
- CPU缓存:每个CPU核心都有独立的L1、L2缓存,共享L3缓存
- 缓存一致性协议:如MESI协议保证多核缓存一致性
- 指令重排序:编译器和处理器为了性能会重排序指令
- 内存屏障:特定指令强制禁用重排序
JMM的作用:
- 屏蔽硬件差异,提供统一的内存访问规范
- 定义 happens-before 原则,保证内存可见性
- 为 synchronized、volatile、final 等关键字提供语义基础
JMM抽象模型 🏗️
关键概念:
- 主内存(Main Memory):存储所有共享变量
- 工作内存(Working Memory):每个线程私有,缓存主内存变量副本
- 内存交互操作:
lock/unlock:锁定/解锁主内存变量read/load:从主内存读取到工作内存use/assign:线程使用/赋值工作内存变量store/write:从工作内存写回主内存
Happens-Before原则 📜
Happens-Before是JMM的核心概念,定义了操作之间的偏序关系,确保内存可见性。
8大Happens-Before规则:
-
程序次序规则:
int a = 1; // 1
int b = 2; // 2
a = b + 1; // 3
// 1 happens-before 2, 2 happens-before 3 -
管程锁定规则:
synchronized(lock) {
// unlock操作 happens-before 后续同一个锁的lock操作
} -
volatile变量规则:
volatile boolean flag = false;
// 对volatile变量的写操作 happens-before 后续对该变量的读操作
flag = true; // 写
if(flag) { // 读 - 保证能看到写操作
} -
线程启动规则:
Thread thread = new Thread(() -> {
// 主线程的启动操作 happens-before 子线程的任何操作
});
thread.start(); -
线程终止规则:
thread.join();
// 子线程的所有操作 happens-before 主线程的join操作返回 -
线程中断规则:
thread.interrupt();
// interrupt()调用 happens-before 检测到中断事件 -
对象终结规则:
// 构造函数的结束 happens-before finalize()的开始 -
传递性:
// A happens-before B, B happens-before C
// 则 A happens-before C
内存屏障(Memory Barriers) 🚧
内存屏障是CPU指令,用于禁止重排序和强制内存刷新。
4种内存屏障:
| 屏障类型 | 作用 | 指令示例 |
|---|---|---|
| LoadLoad Barriers | Load1; LoadLoad; Load2 | 确保Load1数据装载先于Load2 |
| StoreStore Barriers | Store1; StoreStore; Store2 | 确保Store1数据刷新先于Store2 |
| LoadStore Barriers | Load1; LoadStore; Store2 | 确保Load1数据装载先于Store2刷新 |
| StoreLoad Barriers | Store1; StoreLoad; Load2 | 确保Store1数据刷新先于Load2装载 |
volatile的内存屏障语义:
class VolatileExample {
volatile int a = 0;
int b = 0;
void writer() {
b = 1; // 普通写
a = 2; // volatile写 - StoreStore屏障
} // StoreLoad屏障
void reader() {
int i = a; // volatile读 - LoadLoad屏障
int j = b; // 普通读 - LoadStore屏障
}
}
synchronized的内存语义 🔒
同步块的内存语义:
class SynchronizedExample {
int shared = 0;
void increment() {
synchronized(this) {
// 1. 获取锁:清空工作内存,从主内存重新加载
shared++; // 2. 执行操作
// 3. 释放锁:将工作内存写回主内存
}
}
}
关键点:
- 同一时刻只有一个线程能执行同步块
- 进入同步块时:从主内存读取最新值
- 退出同步块时:将修改写回主内存
- 具备 happens-before 保证:解锁 happens-before 后续加锁
volatile关键字详解 ⚡
volatile的特性:
- 可见性:保证读写操作直接操作主内存
- 有序性:禁止指令重排序
- 原子性限制:只保证单次读写的原子性,不保证复合操作
class VolatileCounter {
volatile int count = 0;
// ✅ 可见性保证
void increment() {
count++; // 注意:这不是原子操作!
}
// ✅ 原子性保证(单次读写)
void setCount(int value) {
count = value;
}
// ❌ 非原子性复合操作
void incrementAtomic() {
// 等价于:
// temp = count; // 读
// temp = temp + 1; // 计算
// count = temp; // 写
// 这三步之间可能被其他线程干扰
}
}
volatile的典型使用场景:
状态标记:
class StatusFlag {
volatile boolean shutdownRequested = false;
void shutdown() {
shutdownRequested = true;
}
void doWork() {
while(!shutdownRequested) {
// 工作逻辑
}
}
}
双重检查锁定(DCL):
class Singleton {
private static volatile Singleton instance;
public static Singleton getInstance() {
if(instance == null) { // 第一次检查
synchronized(Singleton.class) {
if(instance == null) { // 第二次检查
instance = new Singleton();
}
}
}
return instance;
}
}
final关键件的内存语义 🔐
final字段的特殊规则:
JDK 5+的final语义:
class FinalFieldExample {
final int x;
int y;
static FinalFieldExample f;
public FinalFieldExample() {
this.x = 3; // final字段在构造函数中初始化
this.y = 4;
}
static void writer() {
f = new FinalFieldExample();
}
static void reader() {
if(f != null) {
int i = f.x; // 保证能看到正确初始化的值
int j = f.y; // 不保证
}
}
}
final的happens-before保证:
- 构造函数内对final字段的写入 happens-before 构造函数外将this引用赋值给引用变量
- 读取final字段时,保证能看到构造函数中写入的值
常见并发问题案例分析 🐛
问题1:可见性问题
// ❌ 错误示例
class VisibilityProblem {
private boolean stop = false;
void startWorker() {
new Thread(() -> {
while(!stop) { // 可能永远看不到stop的变化
// do work
}
}).start();
}
void stopWorker() {
stop = true; // 对工作线程可能不可见
}
}
// ✅ 正确解决方案
class VisibilitySolution {
private volatile boolean stop = false;
void startWorker() {
new Thread(() -> {
while(!stop) { // 保证可见性
// do work
}
}).start();
}
void stopWorker() {
stop = true;
}
}
问题2:指令重排序问题
// ❌ 可能的错误
class ReorderingProblem {
int a = 0, b = 0;
void writer() {
a = 1; // 1
b = 2; // 2 - 可能重排序到1前面
}
void reader() {
while(b == 2 && a != 1) { // 可能发生!
// 观察到 b=2 但 a!=1
}
}
}
// ✅ 解决方案
class ReorderingSolution {
volatile boolean initialized = false;
int a = 0, b = 0;
void writer() {
a = 1;
b = 2;
initialized = true; // volatile写,屏障防止重排序
}
void reader() {
if(initialized) { // volatile读
// 保证能看到 a=1, b=2
assert a == 1 && b == 2;
}
}
}
问题3:复合操作原子性问题
// ❌ 非原子操作
class NonAtomicCounter {
volatile int count = 0;
void increment() {
count++; // 读-改-写,非原子
}
}
// ✅ 解决方案1:使用synchronized
class SynchronizedCounter {
int count = 0;
synchronized void increment() {
count++;
}
}
// ✅ 解决方案2:使用AtomicInteger
class AtomicCounter {
AtomicInteger count = new AtomicInteger(0);
void increment() {
count.incrementAndGet();
}
}
JMM与JVM实现 🔧
HotSpot的实现细节:
缓存行(Cache Line)对齐:
// 避免false sharing
class Padding {
volatile long p1, p2, p3, p4, p5, p6, p7; // 填充
volatile long value;
volatile long p8, p9, p10, p11, p12, p13, p14; // 填充
}
锁消除和锁粗化:
StringBuffer sb = new StringBuffer(); // JIT编译器可能消除锁
sb.append("a").append("b").append("c"); // 可能合并锁
// 粗化前:多次细粒度锁
for(int i = 0; i < list.size(); i++) {
synchronized(list) {
list.get(i);
}
}
// 粗化后:一次粗粒度锁
synchronized(list) {
for(int i = 0; i < list.size(); i++) {
list.get(i);
}
}
性能优化建议 ⚡
volatile vs synchronized选择:
| 场景 | volatile | synchronized |
|---|---|---|
| 简单状态标记 | ✅ 推荐 | ❌ 过重 |
| 复合操作 | ❌ 不适用 | ✅ 保证原子性 |
| 高竞争场景 | ⚠️ CAS重试 | ⚠️ 锁竞争 |
| 读多写少 | ✅ 无锁读取 | ❌ 锁开销 |
内存屏障优化技巧:
// 优化前:每次访问都有内存屏障
class OptimizeVolatile {
volatile long counter;
void increment() {
counter++; // 每次都有内存屏障
}
long getCounter() {
return counter; // 每次都有内存屏障
}
}
// 优化后:减少内存屏障使用
class OptimizeMemoryBarrier {
private long counter;
private volatile boolean dirty;
void increment() {
synchronized(this) {
counter++;
dirty = true; // 只在必要时使用volatile
}
}
long getCounter() {
if(dirty) { // 先检查脏标记
synchronized(this) {
dirty = false;
return counter;
}
}
return counter; // 快速路径,无内存屏障
}
}
面试高频问题 🔥
1. 什么是Java内存模型?
答案要点:
- JMM是Java虚拟机规范中定义的抽象概念
- 规定了线程和主内存之间的交互方式
- 定义了happens-before原则,保证内存可见性
- 为volatile、synchronized、final等关键字提供语义基础
2. volatile和synchronized的区别?
答案要点:
- 作用范围:volatile是变量级别,synchronized是代码块/方法级别
- 原子性:volatile只保证单次读写的原子性,synchronized保证复合操作原子性
- 可见性:两者都保证可见性
- 有序性:两者都禁止指令重排序
- 阻塞:volatile不会阻塞线程,synchronized可能阻塞线程
3. 什么是happens-before原则?
答案要点:
- JMM中定义的偏序关系规则
- 保证前一个操作的结果对后一个操作可见
- 8大规则:程序次序、管程锁定、volatile变量、线程启动/终止等
- 具有传递性
4. DCL为什么要使用volatile?
答案要点:
- 防止指令重排序:
instance = new Singleton()可能重排序为:- 分配内存空间
- 将instance指向内存空间
- 初始化对象
- 重排序后,其他线程可能看到未初始化的对象
- volatile的内存屏障禁止这种重排序
5. final字段的特殊内存语义是什么?
答案要点:
- 构造函数内对final字段的写入happens-before构造函数外将this引用赋值
- 保证其他线程能看到final字段的正确初始化值
- JDK 5+才有的保证,之前版本可能看到默认值
小结
Java内存模型是并发编程的基础理论支撑:
- 理论指导实践:JMM为编写正确并发程序提供理论指导
- 语义保证:volatile、synchronized等关键字基于JMM提供特定语义
- 性能优化:理解JMM有助于进行低级别的性能优化
- 问题诊断:帮助理解并发问题的根源,如可见性问题、重排序问题
掌握JMM不仅是面试必备,更是编写高质量并发程序的基础。在实际开发中,应该优先使用高级并发工具类,但在需要深度优化或理解底层原理时,JMM知识必不可少。