Java内存模型的核心结构
Java内存模型(JMM)定义了Java程序中多线程访问共享变量时的行为规范,其核心设计分为主内存和工作内存两部分。所有线程共享的变量都存储在主内存中,而每个线程拥有独立的工作内存,工作内存中保存了该线程使用到的变量的主内存副本拷贝。

线程对变量的所有操作都必须在工作内存中进行,不能直接读写主内存中的变量。不同线程之间也无法直接访问对方的工作内存,变量值的传递需要通过主内存来完成,这就带来了变量同步的核心问题:如何确保线程修改后的变量能被其他线程及时感知。
变量同步的基础规则
JMM规定了变量在主内存和工作内存之间的8种基本操作,这8种操作都是原子性的,共同构成了变量同步的完整流程:
- lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占的状态
- unlock(解锁):作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才能被其他线程锁定
- read(读取):作用于主内存的变量,把一个变量的值从主内存传输到线程的工作内存中,以便随后的load操作使用
- load(载入):作用于工作内存的变量,把read操作从主内存中得到的变量值放入工作内存的变量副本中
- use(使用):作用于工作内存的变量,把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作
- assign(赋值):作用于工作内存的变量,把一个从执行引擎接收的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作
- store(存储):作用于工作内存的变量,把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用
- write(写入):作用于主内存的变量,把store操作从工作内存中得到的变量的值放入主内存的变量中
这8种操作必须满足以下规则才能保证变量同步的正确性:
- 不允许read和load、store和write操作之一单独出现,即read之后必须load,store之后必须write
- 不允许线程丢弃它最近的assign操作,即工作内存中变量修改之后必须同步回主内存
- 不允许线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存
- 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量
- 一个变量同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次lock之后必须执行相同次数的unlock操作才能解锁
- 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值
- 如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作,也不允许去unlock一个被其他线程锁定的变量
- 对一个变量执行unlock操作之前,必须先把此变量同步回主内存(执行store和write操作)
关键字对变量同步的增强规则
volatile变量的同步规则
volatile是JMM提供的轻量级同步机制,对volatile变量的同步有以下特殊规则:
- 线程对volatile变量的use操作必须与load、read操作连续一起出现,即每次使用volatile变量前都必须从主内存中刷新最新的值,保证能看见其他线程对变量做的修改
- 线程对volatile变量的assign操作必须与store、write操作连续一起出现,即每次修改volatile变量后都必须立刻同步回主内存,保证其他线程能立刻看到修改
- volatile修饰的变量不会被指令重排序优化,保证代码的执行顺序与程序的顺序一致
下面是一段volatile变量的使用示例:
// 定义volatile修饰的共享标志变量
private volatile boolean flag = false;
// 线程A修改flag的值
public void setFlag() {
flag = true; // assign操作后立刻执行store、write同步回主内存
}
// 线程B读取flag的值
public void checkFlag() {
while (!flag) {
// 每次循环都会执行read、load操作从主内存刷新最新值
}
System.out.println("flag已被修改为true");
}
synchronized的同步规则
synchronized关键字通过管程(Monitor)实现同步,对变量同步的规则主要体现在:
- 线程在获取锁(执行lock操作)时,会清空工作内存中该变量的值,后续使用时需要重新从主内存load最新值
- 线程在释放锁(执行unlock操作)前,必须把工作内存中修改的变量同步回主内存(执行store、write操作)
以下是synchronized保证变量同步的示例:
private int count = 0;
public void increment() {
// 获取锁时执行lock操作,清空工作内存中count的副本
synchronized (this) {
// 使用count前会重新从主内存load最新值
count++; // assign修改工作内存中的count副本
// 释放锁前执行unlock操作,会触发store、write把修改后的count写回主内存
}
}
变量同步规则的实际验证
我们可以通过一个没有同步措施的并发场景,来验证变量不同步的问题:
public class VariableSyncTest {
private int num = 0;
public void increase() {
num++;
}
public static void main(String[] args) throws InterruptedException {
VariableSyncTest test = new VariableSyncTest();
// 创建10个线程,每个线程对num执行1000次自增
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
test.increase();
}
}).start();
}
// 等待所有线程执行完成
Thread.sleep(2000);
// 预期结果是10000,实际往往小于10000
System.out.println("最终num的值为:" + test.num);
}
}
上述代码中,num++操作不是原子性的,且多个线程的工作内存中都有num的副本,线程A修改num后没有及时写回主内存,线程B读取到的还是旧值,就会导致最终结果不符合预期。如果给increase方法加上synchronized关键字,或者把num改成volatile变量配合原子操作类,就能保证变量同步的正确性。
常见误区说明
很多开发者认为volatile能保证原子性,这是错误的。volatile只能保证变量的可见性和禁止指令重排序,不能保证复合操作的原子性,比如num++这种包含读取、修改、写入三个步骤的操作,即使使用volatile修饰,多线程下还是会出现同步问题。如果需要保证原子性,还需要配合synchronized或者java.util.concurrent.atomic包下的原子类使用。
另外,工作内存并不是真实的内存区域,而是JMM对CPU缓存、寄存器等的抽象描述,不同硬件架构下的实际实现会有差异,但JMM通过统一的规则屏蔽了硬件差异,让Java程序在不同平台上都能保证一致的内存访问行为。