深入浅出 Java 内存模型(JMM)与 volatile 关键字
在多线程编程中,Java 内存模型(Java Memory Model, JMM)规定了 线程 如何访问 共享变量,并提供了一组规则来保证数据一致性和线程安全。在理解 JMM 之前,我们需要先了解计算机的底层执行机制。
一、Java 内存模型(JMM)的核心概念
1. 为什么需要 JMM?
假设有两个线程 Thread A
和 Thread B
,它们都访问一个共享变量 x
:
int x = 0;
Thread A: x = 10; // 线程 A 修改变量 x
Thread B: System.out.println(x); // 线程 B 读取变量 x
在单线程环境下,x
一定会被更新成 10
,但在多线程环境下,Thread B
可能 看到的是 0
,而不是 10
。这是因为:
- CPU 可能缓存变量 x 的旧值(可见性问题)。
- CPU 可能对指令重新排序(有序性问题)。
- 多个线程可能同时修改 x,导致数据损坏(原子性问题)。
2. JMM 主要解决的问题
Java 内存模型通过 原子性、有序性、可见性 这三个特性,确保多线程环境下的安全访问:
概念 | 定义 | 常见问题 | 如何保证 |
---|---|---|---|
原子性(Atomicity) | 操作不可被中断,要么完整执行,要么不执行 | i++ 不是原子操作,多个线程同时执行可能导致数据错误 | synchronized、Lock、Atomic 类 |
可见性(Visibility) | 线程对变量的修改,其他线程能立即看到 | 线程 A 改变变量,但线程 B 读取到旧值 | volatile、synchronized、Lock |
有序性(Ordering) | 代码的执行顺序与代码编写顺序一致 | 可能发生指令重排,导致预期之外的结果 | volatile、synchronized、happens-before 原则 |
二、volatile 关键字的作用与原理
volatile
是 Java 提供的一种轻量级的同步机制,它主要用于 保证可见性和有序性,但不保证原子性。
1. volatile 关键字的作用
volatile int x = 0;
- 保证可见性:当一个线程修改
x
,其他线程会立即看到最新值。 - 防止指令重排:保证代码执行的顺序和编写顺序一致。
2. volatile 不保证原子性
假设有多个线程执行 x++
操作:
volatile int x = 0;
public void increase() {
x++; // 实际上是 x = x + 1;
}
x++
看似是一个简单的操作,但它其实是三步:
- 读取 x 的值(
load
) - 计算 x+1(
add
) - 写入新值到 x(
store
)
多个线程同时执行 x++
可能会发生竞态条件,导致 x
计算错误:
Thread A: 读取 x = 5
Thread B: 读取 x = 5
Thread A: x = 5 + 1 = 6
Thread B: x = 5 + 1 = 6 // 预期是 7,但变成了 6!
如何解决?
- 用
synchronized
或Lock
保证x++
是原子操作。 - 用
AtomicInteger
替代int
:
AtomicInteger x = new AtomicInteger(0);
x.incrementAndGet(); // 线程安全的自增
三、指令重排与 happens-before 原则
1. 什么是指令重排?
Java 代码的执行顺序 ≠ 代码的编写顺序,因为编译器和 CPU 可能会调整指令顺序以优化性能。例如:
int a = 10;
int b = 20;
int c = a + b;
可能会被重排成:
int b = 20;
int a = 10;
int c = a + b;
虽然在单线程环境下,指令重排不会影响最终结果,但在多线程环境下,它可能导致不可预测的问题。
2. happens-before 原则
为了保证线程间的正确性,Java 规定了 happens-before 关系,即如果操作 A happens-before 操作 B,那么 A 的结果一定对 B 可见。常见规则:
规则 | 示例 |
---|---|
程序次序规则 | 单线程 中,前面的代码 happens-before 后面的代码 |
锁定规则 | unlock() happens-before 之后的 lock() |
volatile 规则 | 对 volatile 变量的写入 happens-before 之后的读取 |
线程启动规则 | Thread.start() happens-before 线程中的代码 |
线程终止规则 | 线程中的代码 happens-before Thread.join() |
传递性 | A happens-before B,B happens-before C,则 A happens-before C |
示例:
volatile int a = 0;
int b = 0;
public void writer() {
a = 1; // (1)
b = 2; // (2)
}
public void reader() {
int x = b; // (3)
int y = a; // (4)
}
由于 a
是 volatile
,保证:
- (1) happens-before (4)
- 但 (2) 不一定 happens-before (3),导致
x=2
时y
可能还是0
四、volatile 底层的内存屏障
为了保证 可见性和有序性,volatile
会在 CPU 层面插入内存屏障(Memory Barrier),让不同 CPU 之间的数据保持一致。
- 写入
volatile
变量 时,JVM 会在指令后插入StoreStore
和StoreLoad
屏障,确保所有写入操作不会被重排序到volatile
变量之后。 - 读取
volatile
变量 时,JVM 会插入LoadLoad
和LoadStore
屏障,确保volatile
变量读取前的所有读操作不会被重排序。
示例:
volatile boolean flag = false;
public void write() {
flag = true; // StoreStore 屏障
}
public void read() {
if (flag) { // LoadLoad 屏障
System.out.println("可见性保证");
}
}
五、总结
问题 | 解法 |
---|---|
如何保证可见性? | volatile 、synchronized 、Lock |
如何防止指令重排? | volatile 、happens-before 原则 |
如何保证原子性? | synchronized 、Lock 、Atomic 变量 |
volatile 适用场景? | 状态标志、双重检查锁(DCL)、轻量级同步 |
Java 内存模型是高性能并发编程的基础,理解 volatile
、指令重排和 happens-before
原则能让我们写出更高效、安全的多线程代码!