面试题答案
一键面试1. Java内存模型中的指令重排机制
- 定义:为了提高性能,编译器和处理器会对指令进行重新排序。指令重排可以在不改变程序执行结果的前提下,调整指令的执行顺序。它分为编译器重排和处理器重排。
- 编译器重排:编译器在编译代码时,会根据目标平台的特性和优化策略,对指令进行重新排序。例如,对于一些不依赖于其他指令结果的独立指令,编译器可能会将它们提前执行。
- 处理器重排:现代处理器采用了超标量、流水线等技术来提高指令执行效率。在执行指令时,处理器可能会根据指令的依赖关系和硬件资源情况,对指令进行动态重排。
2. volatile关键字禁止指令重排的原理
- 内存屏障:volatile关键字通过插入内存屏障(Memory Barrier)来禁止指令重排。内存屏障是一种CPU指令,它会阻止屏障两侧的指令进行重排序。
- Store Memory Barrier(StoreStore Barrier):在volatile写操作之前插入StoreStore Barrier,确保在volatile写之前,所有的普通写操作都已经完成。
- Load Memory Barrier(LoadLoad Barrier):在volatile读操作之后插入LoadLoad Barrier,确保在volatile读之后,所有的普通读操作都不会被重排到volatile读之前。
- StoreLoad Barrier:在volatile写操作之后插入StoreLoad Barrier,这是一个全能型的屏障,它会同时禁止读和写操作的重排。
3. 在复杂多线程并发场景下正确使用volatile关键字防止指令重排引发的程序逻辑错误
- 条件:当一个变量在多线程环境下被频繁读取和修改,并且该变量的状态对程序逻辑至关重要,同时变量的修改不依赖于当前值时,可以使用volatile关键字。
- 注意事项:volatile关键字只能保证可见性和禁止指令重排,并不能保证原子性。所以对于复合操作(如i++),仍然需要使用锁或者原子类(如AtomicInteger)。
4. 错误场景及解决方法示例
- 错误场景:单例模式的双重检查锁定(Double - Checked Locking)在没有volatile关键字时可能出现问题。
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查
instance = new Singleton(); // 问题出在这里
}
}
}
return instance;
}
}
在instance = new Singleton();
这行代码中,可能会被重排为以下顺序:
- 分配内存空间给
Singleton
对象。 - 初始化
Singleton
对象。 - 将
instance
指向分配的内存空间。 如果发生重排,当线程A执行到步骤1和3,但还未执行步骤2时,线程B进入getInstance()
方法,第一次检查instance
不为空,就会返回一个未初始化完全的instance
,导致程序出错。
- 解决方法:使用volatile关键字修饰
instance
变量。
public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查
instance = new Singleton();
}
}
}
return instance;
}
}
这样,由于volatile关键字的内存屏障作用,instance = new Singleton();
这行代码不会发生指令重排,从而保证了单例对象的正确初始化和线程安全。