Java内存模型(JMM)概述
定义与核心作用
Java内存模型(Java Memory Model,JMM)是Java平台定义的一套规范,用于规定多线程环境下程序中共享变量的访问规则。JMM主要解决以下核心问题:
- 可见性问题:确保一个线程对共享变量的修改能够及时被其他线程看到
- 有序性问题:防止编译器和处理器对指令进行重排序导致程序行为异常
- 原子性问题:定义对基本数据类型操作的原子性保证
JMM通过定义happens-before关系来规范线程之间的操作顺序,确保在正确同步的情况下,程序能够表现出预期的行为。例如,对一个volatile变量的写操作happens-before后续对该变量的读操作。
与JVM内存结构的区别
JMM与JVM内存结构是两个不同维度的概念:
-
JMM(逻辑规范):
- 抽象的并发编程模型
- 定义线程如何与内存交互
- 关注变量的可见性、原子性和有序性
- 包含主内存和工作内存的概念
- 规定了同步操作(如synchronized、volatile)的语义
-
JVM内存结构(物理实现):
- 实际的运行时数据区域划分
- 包括堆(Heap)、虚拟机栈(VM Stack)、方法区(Method Area)、程序计数器(PC Register)和本地方法栈(Native Method Stack)
- 堆用于存储对象实例,是所有线程共享的内存区域
- 虚拟机栈存储局部变量表、操作数栈、动态链接和方法出口等信息,是线程私有的
举例说明:当使用synchronized关键字时,JMM层面关注的是它建立的happens-before关系和内存可见性保证;而JVM层面则可能涉及对象头的Mark Word、锁升级过程等具体实现机制。
主内存与工作内存
主内存(Main Memory)
主内存是计算机系统中的共享内存区域,存储着所有可以被多个线程共享的变量。它是线程间通信的主要媒介,具有以下特点:
- 物理上通常是计算机的RAM(随机存取存储器)
- 对所有线程可见,但访问速度相对较慢
- 在Java内存模型中,所有实例字段、静态字段和数组元素都存储在主内存中
工作内存(Working Memory)
工作内存是每个线程私有的数据存储区域,具有以下特性:
- 存储线程操作中使用的变量副本(从主内存加载而来)
- 物理上可能对应CPU缓存或寄存器
- 访问速度比主内存快
- 每个线程只能直接访问自己的工作内存,不能直接访问其他线程的工作内存
交互协议
线程与主内存的交互通过以下8种基本操作完成:
lock(锁定):作用于主内存变量,标识变量为线程独占状态unlock(解锁):作用于主内存变量,释放锁定状态read(读取):从主内存传输变量到工作内存load(载入):把read操作得到的值放入工作内存的变量副本中use(使用):把工作内存变量值传递给执行引擎assign(赋值):将执行引擎接收到的值赋给工作内存变量store(存储):把工作内存变量值传送到主内存write(写入):把store操作得到的值放入主内存变量中
原子性、可见性与有序性
原子性(Atomicity)
原子性指的是一个操作是不可中断的,要么全部执行完成,要么完全不执行。
- 基本数据类型的读写操作是原子的(除了long和double的非volatile变量)
- 复合操作如i++(读取-修改-写入)不是原子的
- 保证原子性的方法:
- 使用synchronized同步块
- 使用java.util.concurrent.atomic包中的原子类(如AtomicInteger)
- 使用显式锁(如ReentrantLock)
示例:
java
// 非原子操作
int i = 0;
i++; // 不是原子操作
// 原子操作解决方案
AtomicInteger atomicInt = new AtomicInteger(0);
atomicInt.incrementAndGet(); // 原子操作
可见性(Visibility)
可见性指的是当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。
- 保证可见性的机制:
volatile关键字:强制从主内存读取变量,修改后立即写回主内存synchronized同步块:解锁前将所有变量写回主内存final关键字:正确初始化的final字段对其他线程可见
示例:
java
// 可见性问题示例
boolean flag = false; // 可能引发可见性问题
// 解决方案1: volatile
volatile boolean volatileFlag = false;
// 解决方案2: synchronized
synchronized void setFlag(boolean value) {
flag = value;
}
有序性(Ordering)
有序性指的是程序执行的顺序按照代码的先后顺序执行。
- 编译器/处理器可能会对指令进行重排序优化
- 保证有序性的方法:
volatile关键字:通过内存屏障禁止特定类型的重排序synchronized同步块:临界区内禁止指令重排序- happens-before规则
Happens-Before原则
程序顺序规则
在单个线程内,按照程序代码顺序,前面的操作happens-before后面的操作。
示例:
java
int x = 1; // 操作A
int y = 2; // 操作B
// 在单线程中,A happens-before B
锁规则
一个unlock操作happens-before后续对同一个锁的lock操作。
示例:
java
synchronized(lock) {
// 操作A
}
// unlock happens-before 下一个lock
synchronized(lock) {
// 操作B
}
volatile规则
对一个volatile变量的写操作happens-before后续对这个变量的读操作。
示例:
java
volatile boolean flag = false;
// 线程1
flag = true; // 写操作
// 线程2
if(flag) { // 读操作
// 保证能看到线程1的修改
}
线程启动/终止规则
- 线程A启动线程B,那么A在启动B前的所有操作对B可见
- 线程A等待线程B终止(通过Thread.join()),那么B的所有操作对A可见
传递性规则
如果A happens-before B,且B happens-before C,那么A happens-before C。
同步机制与内存屏障
synchronized
synchronized关键字通过内置锁(monitor)实现同步:
- 进入同步块时:
- 获取锁
- 清空工作内存
- 从主内存重新加载变量
- 退出同步块时:
- 将工作内存中的变量写回主内存
- 释放锁
内存屏障:
- 在monitorenter指令后插入LoadLoad和LoadStore屏障
- 在monitorexit指令前插入StoreStore和StoreLoad屏障
volatile
volatile变量的特殊规则:
- 每次读取前强制从主内存刷新最新值
- 每次写入后立即刷新到主内存
- 禁止指令重排序
内存屏障:
- 在volatile写操作前插入StoreStore屏障
- 在volatile写操作后插入StoreLoad屏障
- 在volatile读操作后插入LoadLoad和LoadStore屏障
final
final字段的特殊语义:
- 构造函数内对final字段的写入与随后将被构造对象的引用赋值给一个引用变量之间不能重排序
- 初次读取包含final字段的对象引用,与随后初次读取这个final字段之间不能重排序
- 保证正确构造的final字段对所有线程可见,无需同步
示例:
java
final class FinalExample {
final int x;
int y;
public FinalExample() {
x = 1; // final字段写
y = 2; // 普通字段写
}
// 其他线程看到FinalExample实例时,保证能看到x=1
// 但不保证能看到y=2(除非有其它同步措施)
}