知识模块
☕ Java 知识模块
三、Java 并发编程
Java 内存模型(JMM)

Java 内存模型(JMM)

JMM(Java Memory Model)定义了 Java 程序中变量的访问规则,是理解并发编程的关键。

为什么需要 JMM?

多线程环境下存在三大问题:

  1. 可见性:一个线程修改了变量,其他线程可能看不到
  2. 原子性:操作可能被中断,不是原子的
  3. 有序性:代码执行顺序可能与编写顺序不同

主内存与工作内存

JMM 抽象模型:

     线程1              线程2
       │                  │
   工作内存            工作内存
   (副本)              (副本)
       │                  │
       └────────┬─────────┘

            主内存
           (共享变量)
  • 主内存:存储所有共享变量(实例变量、静态变量)
  • 工作内存:每个线程独有,存储变量的副本

线程不能直接操作主内存,必须通过工作内存。

内存可见性问题

public class VisibilityDemo {
    boolean running = true;  // 主内存中的共享变量
    
    public void stop() {
        running = false;  // 线程 A 修改
    }
    
    public void doWork() {
        while (running) {  // 线程 B 读取工作内存副本
            // 可能永远无法停止!
        }
    }
}

原因:线程 B 读取的是工作内存中的副本,线程 A 修改后未同步到主内存。

解决:使用 volatile 关键字。

volatile boolean running = true;  // 保证可见性

指令重排序

编译器和处理器为了优化性能,会对指令进行重排序。

重排序类型

类型说明
编译器重排序编译器优化代码执行顺序
处理器重排序CPU 指令级并行优化
内存系统重排序缓存不一致导致的可见性问题

重排序示例

int a = 0;
boolean flag = false;
 
// 线程 A
public void writer() {
    a = 1;       // 1
    flag = true; // 2
}
 
// 线程 B
public void reader() {
    if (flag) {  // 3
        int i = a; // 4 - 可能读到 0!
    }
}

由于重排序,操作 1 和 2 可能被交换顺序,导致线程 B 看到 flag=true 但 a=0。

happens-before 原则

JMM 通过 happens-before 原则保证有序性。如果操作 A happens-before 操作 B,则 A 的结果对 B 可见。

八大原则

原则说明
程序顺序规则同一线程中,前面的操作 happens-before 后面的操作
监视器锁规则unlock happens-before 后续的 lock
volatile 规则volatile 写 happens-before 后续的 volatile 读
线程启动规则Thread.start() happens-before 该线程的每个动作
线程终止规则线程中的所有操作 happens-before 其他线程检测到终止
线程中断规则interrupt() happens-before 被中断线程检测到中断
对象终结规则构造函数 happens-before finalizer 的开始
传递性规则A hb B,B hb C → A hb C

示例分析

class HappensBeforeExample {
    int x = 0;
    volatile boolean v = false;
    
    // 线程 A
    public void writer() {
        x = 42;    // 1
        v = true;  // 2 (volatile 写)
    }
    
    // 线程 B
    public void reader() {
        if (v) {   // 3 (volatile 读)
            // x == 42 保证可见!
            // 因为 1 happens-before 2 (程序顺序规则)
            // 2 happens-before 3 (volatile 规则)
            // 1 happens-before 4 (传递性)
        }
    }
}

volatile 实现原理

可见性实现

volatile 写操作会强制刷新到主内存,volatile 读操作会强制从主内存读取。

volatile 写:
1. 将工作内存中的值刷新到主内存
2. 使其他线程的工作内存中的缓存失效

volatile 读:
1. 从主内存读取最新值到工作内存
2. 使用工作内存中的值

有序性实现

volatile 通过内存屏障(Memory Barrier)禁止重排序:

屏障类型说明
LoadLoad禁止 Load1 和 Load2 重排序
StoreStore禁止 Store1 和 Store2 重排序
LoadStore禁止 Load 和 Store 重排序
StoreLoad禁止 Store 和 Load 重排序

JMM 对 volatile 的插入规则:

  • volatile 写前:插入 StoreStore 屏障
  • volatile 写后:插入 StoreLoad 屏障
  • volatile 读后:插入 LoadLoad 和 LoadStore 屏障

synchronized 实现原理

synchronized 通过监视器锁(Monitor)实现原子性和可见性:

synchronized (lock) {
    // 1. 获取锁(lock)
    // 2. 清空工作内存,从主内存读取共享变量
    // 3. 执行代码
    // 4. 将修改刷新到主内存
    // 5. 释放锁(unlock)
}

happens-before 保证:unlock happens-before 后续的 lock。

final 域的内存语义

final 域有特殊的初始化安全保证:

class FinalExample {
    final int x;
    int y;
    
    public FinalExample() {
        x = 3;  // final 域写入
        y = 4;  // 普通域写入
    }
}
 
// 线程 A
obj = new FinalExample();
 
// 线程 B
if (obj != null) {
    // 保证看到 x = 3(final 保证)
    // y 可能是 0 或 4(普通域不保证)
}

final 语义:构造函数完成前,final 域的写入保证在对象引用对其他线程可见之前完成。

内存屏障与 CPU

不同 CPU 对内存屏障的支持不同:

CPU屏障类型
x86sfence、lfence、mfence
ARMdmb、dsb、isb
SPARCmembar

JMM 屏蔽了底层差异,提供统一的内存模型。

常见面试题

Q1: JMM 和 JVM 运行时数据区是什么关系?

  • JMM 是抽象模型,定义多线程访问规则
  • JVM 运行时数据区是具体实现,包括堆、栈等
  • 工作内存对应 CPU 缓存/寄存器,主内存对应堆内存

Q2: volatile 能保证原子性吗?

不能。volatile 只保证可见性和有序性,不保证原子性。

volatile int count = 0;
count++;  // 不是原子操作!读-改-写三步

Q3: happens-before 和时间先后是什么关系?

happens-before 是逻辑顺序,不是时间先后。A happens-before B 不意味着 A 先于 B 执行,而是保证 A 的结果对 B 可见。

Q4: 为什么需要内存屏障?

CPU 和编译器会进行优化重排序,内存屏障告诉编译器和 CPU 不要重排序特定操作,保证多线程正确性。

小结

  • JMM 解决可见性、原子性、有序性问题
  • 主内存存储共享变量,工作内存存储副本
  • happens-before 定义操作的可见性关系
  • volatile 通过内存屏障实现可见性和有序性
  • synchronized 通过监视器锁实现原子性和可见性
  • final 域保证初始化安全