Java高频面试之并发编程-14
hello啊,各位观众姥爷们!!!本baby今天又来报道了!哈哈哈哈哈嗝🐶
面试官:指令重排有限制没有?happens-before 又是什么?
在并发编程中,指令重排(Instruction Reordering) 和 happens-before 原则是确保多线程程序正确性的核心机制。
一、指令重排的限制
1. 指令重排的定义
编译器或处理器为了优化性能,可能在不改变单线程执行结果的前提下,调整指令的执行顺序。例如:
int a = 1; // 指令1
int b = 2; // 指令2
编译器可能先执行指令2再执行指令1,因为这对单线程结果无影响。
2. 重排的限制
在多线程环境下,无限制的指令重排会导致数据不一致。因此,JMM(Java内存模型)通过 happens-before原则 限制重排,确保以下操作的顺序性:
- 写后读:若操作A写变量,操作B读该变量,则A必须在B之前。
- 写后写:若操作A和B都写同一变量,A必须在B之前。
- 读后写/读:若操作A读变量,操作B写或读该变量,则需确保可见性。
3. 禁止重排的场景
- volatile变量:对
volatile
变量的读写操作不能被重排。 - 锁操作:加锁(
monitorenter
)和解锁(monitorexit
)之间的代码不能被重排到锁外。 - final字段:构造函数中对
final
字段的写入,不会被重排到构造函数外。
二、happens-before原则
1. 定义
happens-before是JMM定义的操作间的偏序关系,确保前一个操作的结果对后续操作可见。若操作A happens-before操作B,则:
- A的执行结果对B可见。
- A的执行顺序排在B之前。
2. 核心规则
规则 | 描述 | 示例 |
---|---|---|
程序顺序规则 | 单线程中,代码书写顺序的操作happens-before后续操作。 | int x=1; int y=x+1; → x=1 happens-before y=x+1 。 |
volatile规则 | 对volatile 变量的写操作happens-before后续对该变量的读操作。 | volatile boolean flag=false; → flag=true 写happens-beforeflag 读。 |
锁规则 | 解锁操作happens-before后续的加锁操作。 | synchronized(lock){...} → 解锁happens-before下一个线程的加锁。 |
线程启动规则 | 线程的start() 调用happens-before该线程内的任何操作。 | thread.start() → 线程内run() 中的操作可见start() 前的修改。 |
线程终止规则 | 线程中的所有操作happens-before其他线程检测到该线程终止(如join() 返回)。 | thread.join() → 主线程能看到线程内所有修改。 |
传递性规则 | 若A happens-before B,且B happens-before C,则A happens-before C。 | 用于链式操作的有序性保证。 |
3. 实际应用
-
双重检查锁定(DCL)
public class Singleton {private static volatile Singleton instance; // 必须用volatilepublic static Singleton getInstance() {if (instance == null) {synchronized (Singleton.class) {if (instance == null) {instance = new Singleton(); // 禁止重排初始化与赋值}}}return instance;} }
volatile
防止指令重排,确保对象完全初始化后才赋值给instance
。 -
线程间通信
volatile boolean flag = false; // 线程1 data = 42; flag = true; // 写操作happens-before线程2的读操作 // 线程2 while (!flag); // 等待flag为true System.out.println(data); // 确保看到data=42
volatile
保证线程1的写操作对线程2可见。
三、happens-before与内存屏障
1. 内存屏障的作用
JVM通过插入内存屏障(Memory Barrier)指令实现happens-before规则,限制指令重排。常见屏障类型:
- LoadLoad:确保当前读操作前的所有读操作完成。
- StoreStore:确保当前写操作前的所有写操作完成。
- LoadStore:确保当前读操作后的所有写操作完成。
- StoreLoad:确保当前写操作后的所有读/写操作完成(开销最大)。
2. volatile的实现
- 写操作:在写
volatile
变量后插入StoreStore
和StoreLoad
屏障。 - 读操作:在读
volatile
变量前插入LoadLoad
和LoadStore
屏障。
四、总结
机制 | 核心作用 | 关键点 |
---|---|---|
指令重排 | 优化性能,但受限于happens-before规则。 | 单线程结果不变,多线程需同步机制保证可见性。 |
happens-before | 定义操作间的可见性和顺序性,限制指令重排。 | 通过程序顺序、锁、volatile等规则确保多线程正确性。 |
内存屏障 | 物理实现happens-before规则,强制刷新内存和限制重排。 | volatile、锁等同步机制的底层依赖。 |
开发建议:
- 优先使用
volatile
、synchronized
、原子类等工具显式管理同步。 - 理解happens-before规则,避免隐式依赖指令顺序。
- 复杂场景结合工具(如
java.util.concurrent
包)简化并发控制。