跳到主要内容

Java内存模型(JMM)

Java内存模型(Java Memory Model, JMM)是一个抽象的概念,它定义了线程和主内存之间的抽象关系,以及多线程环境下共享变量的读写规则。理解JMM对于编写正确的并发程序至关重要。


为什么需要JMM?🤔

硬件内存架构的复杂性

  • CPU缓存:每个CPU核心都有独立的L1、L2缓存,共享L3缓存
  • 缓存一致性协议:如MESI协议保证多核缓存一致性
  • 指令重排序:编译器和处理器为了性能会重排序指令
  • 内存屏障:特定指令强制禁用重排序

JMM的作用

  • 屏蔽硬件差异,提供统一的内存访问规范
  • 定义 happens-before 原则,保证内存可见性
  • 为 synchronized、volatile、final 等关键字提供语义基础

JMM抽象模型 🏗️

关键概念:

  1. 主内存(Main Memory):存储所有共享变量
  2. 工作内存(Working Memory):每个线程私有,缓存主内存变量副本
  3. 内存交互操作
    • lock/unlock:锁定/解锁主内存变量
    • read/load:从主内存读取到工作内存
    • use/assign:线程使用/赋值工作内存变量
    • store/write:从工作内存写回主内存

Happens-Before原则 📜

Happens-Before是JMM的核心概念,定义了操作之间的偏序关系,确保内存可见性。

8大Happens-Before规则:

  1. 程序次序规则

    int a = 1;    // 1
    int b = 2; // 2
    a = b + 1; // 3
    // 1 happens-before 2, 2 happens-before 3
  2. 管程锁定规则

    synchronized(lock) {
    // unlock操作 happens-before 后续同一个锁的lock操作
    }
  3. volatile变量规则

    volatile boolean flag = false;
    // 对volatile变量的写操作 happens-before 后续对该变量的读操作
    flag = true; // 写
    if(flag) { // 读 - 保证能看到写操作
    }
  4. 线程启动规则

    Thread thread = new Thread(() -> {
    // 主线程的启动操作 happens-before 子线程的任何操作
    });
    thread.start();
  5. 线程终止规则

    thread.join();
    // 子线程的所有操作 happens-before 主线程的join操作返回
  6. 线程中断规则

    thread.interrupt();
    // interrupt()调用 happens-before 检测到中断事件
  7. 对象终结规则

    // 构造函数的结束 happens-before finalize()的开始
  8. 传递性

    // A happens-before B, B happens-before C
    // 则 A happens-before C

内存屏障(Memory Barriers) 🚧

内存屏障是CPU指令,用于禁止重排序和强制内存刷新。

4种内存屏障:

屏障类型作用指令示例
LoadLoad BarriersLoad1; LoadLoad; Load2确保Load1数据装载先于Load2
StoreStore BarriersStore1; StoreStore; Store2确保Store1数据刷新先于Store2
LoadStore BarriersLoad1; LoadStore; Store2确保Load1数据装载先于Store2刷新
StoreLoad BarriersStore1; 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的特性:

  1. 可见性:保证读写操作直接操作主内存
  2. 有序性:禁止指令重排序
  3. 原子性限制:只保证单次读写的原子性,不保证复合操作
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选择:

场景volatilesynchronized
简单状态标记✅ 推荐❌ 过重
复合操作❌ 不适用✅ 保证原子性
高竞争场景⚠️ 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()可能重排序为:
    1. 分配内存空间
    2. 将instance指向内存空间
    3. 初始化对象
  • 重排序后,其他线程可能看到未初始化的对象
  • volatile的内存屏障禁止这种重排序

5. final字段的特殊内存语义是什么?

答案要点

  • 构造函数内对final字段的写入happens-before构造函数外将this引用赋值
  • 保证其他线程能看到final字段的正确初始化值
  • JDK 5+才有的保证,之前版本可能看到默认值

小结

Java内存模型是并发编程的基础理论支撑:

  1. 理论指导实践:JMM为编写正确并发程序提供理论指导
  2. 语义保证:volatile、synchronized等关键字基于JMM提供特定语义
  3. 性能优化:理解JMM有助于进行低级别的性能优化
  4. 问题诊断:帮助理解并发问题的根源,如可见性问题、重排序问题

掌握JMM不仅是面试必备,更是编写高质量并发程序的基础。在实际开发中,应该优先使用高级并发工具类,但在需要深度优化或理解底层原理时,JMM知识必不可少。