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