可见性、有序性、原子性三体问题
指令重排序
- 编译器优化指令重排序
Java 源码转化为字节码或本地机器码时,可能会进行指令重排序提高执行效率,但只保证单线程语义
- 消除冗余加载
java
// 原始代码
int a = obj.x;
// ...其他不依赖a的操作
int b = obj.x; // 可能会被优化成直接复用寄存器中的a值
// 优化后逻辑
int tmp = obj.x;
int a = tmp;
// ...中间代码
int b = tmp;
- 指令调度优化
java
// 原始顺序
int x = 1;
int y = 2; // 无数据依赖的两个写操作可能被交换
// 可能优化后的顺序
int y = 2;
int x = 1;
- 循环展开
java
if (instance == null) { // 第一次检查
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查
instance = new Singleton(); // 这里可能被拆分为:1.分配内存 2.初始化 3.赋值引用(步骤2和3可能被重排)
}
}
}
- 处理器乱序执行
Java 中通过 volatile
写操作插入 StoreStore + StoreLoad
屏障
- 内存系统重排序
JDK9 引入 VarHandle
来替代 Unsafe
提供更细粒度的内存顺序控制
可见性
- 当写一个
volatile
变量时, JMM 会把该线程对应的本地内存中的共享变量值 立即刷新到主内存 中 - 当读一个
volatile
变量时, JMM 会把该线程对应的本地内存设置为无效,重新回到主内存中读取最新共享变量
有序性
内存屏障
内存屏障,分为 Store Barrier
(写屏障) 和 Load Barrier
(读屏障)
- 读屏障:在读指令之前插入读屏障,让工作内存或CPU缓存中的数据失效,重新回到主内存中读取最新数据
- 写屏障:在写指令之后插入写屏障,强制把写缓冲中的数据刷新到主内存中
屏障类型 | 指令示例 | 说明 |
---|---|---|
LoadLoad | Load1;LoadLoad;Load2 | Load1 执行完之后 Load2 再执行 |
StoreStore | Store1;StoreStore;Store2 | Store1 执行完之后, Store2 再执行 |
LoadStore | Load1;LoadStore;Store1 | 先读完再写 |
StoreLoad | Store1;StoreLoad;Load1 | 先写完再读 |
- 每一个
volatile
写操作前面插入一个StoreStore
屏障 - 每一个
volatile
写操作后面插入一个StoreLoad
屏障 - 每一个
volatile
读操作之前插入一个LoadLoad
屏障 - 每一个
volatile
读操作之后插入一个LoadStore
屏障
如何保证有序性?
- 对于编译器的重排序, JMM 会根据重排序的规则,禁止特定类型的编译器重排序
- 对于处理器的重排序, JAVA 编译器在生成指令序列的适当位置,插入内存屏障指令,来禁止特定类型的处理器重排序
原子性
Volatile 不保证原子性,需要配合锁或原子类来保证原子性
可见性保证 ≠ 原子性保证
java
// Thread-1 Thread-2
读取 i=0 → 读取 i=0 →
计算 i=1 → 计算 i=1 →
写回 i=1 写回 i=1
- 虽然
volatile
保证了写操作对后续读取线程立即可见,但两个线程在 "读取-计算-写入" 过程中仍会产生竞态条件 - 最终
i
的值可能为1
,而非预期的2
volatile 适用场景
- 单一赋值操作
- 状态标志位 (如:
isRunning
) - 开销较低的读、写锁策略
java
public class Counter {
//当读远多于写时,使用 volatile 保证读取操作的可见性,利用 synchronized 保证复合操作的原子性
private volatile int value;
public int getValue() {
return value;
}
public synchronized int increment() {
return value++;
}
}
- DSL (Double-Checked-Locking) 单例
java
public class SafeDoubleCheckSingleton {
// ⚡️ 关键1:使用 volatile 修饰实例变量
private volatile static SafeDoubleCheckSingleton singleton;
private SafeDoubleCheckSingleton() {
//防止反射破坏单例
if (singleton != null) {
throw new RuntimeException("禁止通过反射创建实例")
}
}
public static SafeDoubleCheckSingleton getInstance() {
// 第一次检查非同步,减少锁竞争
if (singleton == null) {
synchronized (SafeDoubleCheckSingleton.class) {
// 第二次检查同步,保证线程安全
if (singleton == null) {
singleton = new SafeDoubleCheckSingleton();
}
}
}
return singleton;
}
}
总结:
volatile
写之前的操作,都禁止重排序到volatile
之后 ----- 保证所有前置操作对读线程可见volatile
读之后的操作,都禁止重排序到volatile
之前 ----- 保证后续操作基于最新数据执行volatile
写之后volatile
读,禁止重排序 ----- 防止线程读取到正在写入中的中间状态
实战应用面试题
DSL 单例模式为什么必须使用 volatile ?
java
// 分解为 1.分配内存 2.初始化对象 3.赋值引用
instance = new Singleton();
- 若发生指令重排,其他线程可能获取到未完成初始化的对象
是否可用 volatile 修饰数组?
可以修饰数组引用,但不保证数组元素的可见性
happens-before 八大原则
JDK5 重写了 JAVA 内存模型(JMM),定义了 Happens-Before 规则:
尽管编译器/CPU 会进行重排序,但也必须保证符合规则的可见性和顺序性
- 单线程规则
同一线程内,操作 ② 的最终执行顺序一定在操作 ① 之后
java
int a = 1; // 操作 ①
int b = a + 2; // 操作 ②
- 管程锁定规则
JVM 在释放锁时强制刷新写缓冲区的数据到主内存;获取锁时本地缓存失效
java
synchronized(lock) { // A线程释放锁
x = 42;
}
// 其他线程获取同一锁时
synchronized(lock) { // B线程获得锁
int tmp = x; // 必定看到x=42
}
- volatile 变量规则
volatile 变量的可见性保证
java
volatile boolean flag = false;
// 线程A
void write() {
flag = true; // volatile写
}
// 线程B
void read() {
if (flag) { // volatile读,必定看到线程A的写入
// 安全操作
}
}
- 线程启动规则
java
Thread t = new Thread(() -> {
System.out.println(data); // 能正确看到主线程设置的初始值
});
// 主线程修改共享数据
data = initData;
t.start(); // start()调用 happens-before 子线程的任何操作
- 线程终止规则
join()
确保子线程修改对主线程可见
java
Thread t = new Thread(() -> {
x = 42;
});
t.start();
t.join(); // 主线程在此等待
System.out.println(x); // 确保看到x=42
注意:join()
不会自动清除中断状态,异常处理要清理中断状态,以免影响后续代码
java
try {
t.join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // 恢复中断标志
// 处理中断逻辑
}
- 中断规则
java
Thread t = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
// 执行任务
}
// 正确处理中断
});
t.start();
t.interrupt(); // happens-before 中断状态被检测到
- 终结器规则
finalizer
本身设计存在问题,这条规则没多大实际意义
- 传递性规则
借助 volatile
变量建立跨线程的 happens-before
链,是解决复杂可见性问题的核心
java
// 若 A happens-before B 且 B happens-before C,则 A happens-before C
volatile int x = 0;
int y = 0;
// 线程A
x = 1; // volatile写
// 线程B
if(x == 1) { // volatile读,建立happens-before
y = 42; // 此时写y对线程C可见
}
// 线程C
if(y == 42) {
// 保证能看到线程A对x的写入
}