Java JMM
在多线程编程中,线程对共享变量的读写操作可能因CPU缓存、编译器优化等导致数据不一致问题。JMM(Java Memory Model,Java内存模型)并非物理内存结构,而是JVM定义的一套内存访问规范,其核心目标是屏蔽不同硬件和操作系统的内存差异,保证Java程序在多线程环境下的内存可见性、原子性和有序性,让程序在不同平台上表现出一致的并发行为。
# 一、JMM的核心模型:主内存与工作内存
JMM通过抽象“主内存”和“工作内存”,规范了线程对共享变量的访问方式。
# 1.1 主内存(Main Memory)
- 定义:所有线程共享的内存区域,是JVM对物理内存的抽象(并非完全等同于物理内存,可能包含JVM堆、方法区等共享数据区域)。
- 存储内容:所有共享变量(如实例变量、静态变量等,局部变量因线程私有不属此类)。
# 1.2 工作内存(Working Memory)
- 定义:每个线程独有的内存区域,对应CPU的高速缓存、寄存器等硬件存储结构。
- 存储内容:线程需要操作的共享变量的副本(线程不能直接读写主内存的共享变量,必须通过副本间接操作)。
# 1.3 线程与内存的交互规则
线程对共享变量的操作需遵循以下步骤(JMM定义的原子操作):
- 读取(read):从主内存将共享变量的值读到工作内存。
- 加载(load):将read得到的值加载到工作内存的变量副本中。
- 使用(use):线程执行时,从工作内存的变量副本中读取值并使用(如计算)。
- 赋值(assign):线程修改变量后,将新值赋值给工作内存的变量副本。
- 存储(store):将工作内存中修改后的变量值传递到主内存。
- 写入(write):将store得到的值写入主内存的共享变量中。
示例流程:
[主内存] → 共享变量a=0
↓(线程1执行read→load) ↓(线程2执行read→load)
[线程1工作内存] → a=0副本 [线程2工作内存] → a=0副本
↓(线程1执行use→assign:a=1) ↓(线程2执行use:读取到a=0)
[线程1工作内存] → a=1副本 [线程2工作内存] → a=0副本(可见性问题)
↓(线程1执行store→write)
[主内存] → a=1(更新后的值)
2
3
4
5
6
7
注意:不同线程的工作内存相互隔离,变量值的传递必须通过主内存,这是多线程内存问题的根源。
# 二、JMM解决的三大核心问题
多线程并发时,因主内存与工作内存的交互机制,可能出现可见性、原子性、有序性问题,JMM通过关键字和底层机制解决这些问题。
# 2.1 可见性问题:线程间“看不到”变量更新
# 问题定义
当线程A修改了共享变量并同步到主内存前,线程B读取的仍是工作内存中未更新的旧副本,导致数据不一致。
# 问题示例
private static boolean flag = false; // 主内存中的共享变量
public static void main(String[] args) throws InterruptedException {
// 线程1:1秒后将flag改为true
new Thread(() -> {
try { Thread.sleep(1000); } catch (InterruptedException e) {}
flag = true; // 修改工作内存副本,尚未同步到主内存
System.out.println("线程1:flag已设为true");
}).start();
// 线程2:循环判断flag,若为true则退出
new Thread(() -> {
while (!flag) {
// 始终读取工作内存中的旧副本(false),无法感知线程1的修改
}
System.out.println("线程2:检测到flag为true,退出循环");
}).start();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
现象:线程2可能永远无法退出循环(因看不到flag的更新)。
# 解决方案
JMM通过强制刷新工作内存与主内存的同步,保证可见性:
- volatile关键字:
- 写操作:线程修改volatile变量后,会立即通过写屏障将工作内存的新值同步到主内存。
- 读操作:线程读取volatile变量前,会通过读屏障从主内存刷新最新值到工作内存。
- synchronized关键字:
- 释放锁时:线程会将工作内存的变量值同步回主内存(写屏障作用)。
- 获取锁时:线程会清空工作内存,从主内存重新加载变量(读屏障作用)。
# 2.2 原子性问题:操作被中断导致数据错误
# 问题定义
一个操作或多个操作要么全部执行且执行过程不被中断,要么都不执行。若操作被多线程打断,会导致数据不一致(如count++实际是“读-改-写”三步操作,可能被其他线程插入执行)。
# 问题示例
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
// 10000个线程同时执行count++
for (int i = 0; i < 10000; i++) {
new Thread(() -> {
try { Thread.sleep(1); } catch (InterruptedException e) {} // 放大竞争概率
count++; // 非原子操作:读取count→加1→写入count
}).start();
}
Thread.sleep(1000); // 等待所有线程执行完毕
System.out.println("最终count值:" + count); // 结果通常<10000
}
2
3
4
5
6
7
8
9
10
11
12
13
现象:最终结果小于10000,因多个线程同时读取旧值并修改,覆盖了彼此的结果。
# 解决方案
JMM通过限制并发执行范围或使用原子指令保证原子性:
- synchronized关键字:通过互斥锁保证临界区(同步代码块)内的代码仅被一个线程执行,避免操作被中断。
- 原子类(如AtomicInteger):基于CPU的CAS(Compare-And-Swap)指令实现原子操作,无需加锁。例如
AtomicInteger.incrementAndGet()通过底层Unsafe类的compareAndSwapInt方法,原子性地完成“读-改-写”。
# 2.3 有序性问题:指令重排序破坏逻辑
# 问题定义
编译器或CPU为优化性能,会调整指令的执行顺序(重排序)。单线程下重排序不影响结果,但多线程下可能破坏代码逻辑。
# 问题示例
private static int a = 0;
private static boolean b = false;
public static void main(String[] args) {
// 线程1:修改a和b
new Thread(() -> {
a = 10; // 操作1
b = true; // 操作2(可能被重排序到操作1前执行)
}).start();
// 线程2:依赖b的状态读取a
new Thread(() -> {
if (b) { // 若b=true,预期a=10
System.out.println("a: " + a); // 可能输出a=0(因操作2先执行)
}
}).start();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
现象:因重排序,线程1可能先执行b=true再执行a=10,此时线程2读取到b=true但a=0,违背逻辑预期。
# 解决方案
JMM通过禁止特定重排序保证有序性:
- volatile关键字:通过内存屏障禁止volatile变量与其他变量的重排序(如
volatile变量的写操作不能被重排序到之前的操作前,读操作不能被重排序到之后的操作后)。 - synchronized关键字:同步代码块内的指令执行顺序与代码逻辑一致(因互斥性,单线程执行无重排序问题)。
- final关键字:禁止final字段的初始化与构造函数外的操作重排序。例如,
final变量在构造函数中初始化后,其他线程看到的一定是初始化后的值(不会看到未初始化的中间状态)。
# 三、内存屏障:JMM的底层实现机制
JMM通过内存屏障(Memory Barrier) 强制约束指令重排序和内存可见性,是解决三大问题的底层保障。
# 3.1 内存屏障的分类与作用
- LoadLoad屏障:禁止屏障后的读操作重排序到屏障前(确保先读完屏障前的变量,再读屏障后的变量)。
- StoreStore屏障:禁止屏障后的写操作重排序到屏障前(确保先写完屏障前的变量,再写屏障后的变量)。
- LoadStore屏障:禁止屏障后的写操作重排序到屏障前的读操作前(确保读完后再写)。
- StoreLoad屏障:禁止屏障后的读操作重排序到屏障前的写操作前(最严格,确保写完后再读,会刷新缓存)。
# 3.2 Java中内存屏障的应用
- volatile关键字:
- 写
volatile变量后:插入StoreStore屏障(保证之前的写操作已同步到主内存)和StoreLoad屏障(保证写操作完成后再执行后续读操作)。 - 读
volatile变量前:插入LoadLoad屏障(保证先读主内存最新值)和LoadStore屏障(保证读完后再执行后续写操作)。
- 写
- synchronized关键字:
- 获取锁时:插入LoadLoad屏障和LoadStore屏障(清空工作内存,从主内存加载最新值)。
- 释放锁时:插入StoreStore屏障和StoreLoad屏障(将工作内存的修改同步回主内存)。
# 四、总结
JMM作为Java多线程内存交互的核心规范,通过抽象主内存与工作内存模型,解决了多线程并发的三大问题:
- 可见性:通过volatile、synchronized的内存同步机制保证。
- 原子性:通过synchronized的互斥锁或原子类的CAS指令保证。
- 有序性:通过volatile、synchronized、final的重排序约束保证。