题目
深入理解Java内存模型:volatile与synchronized在双重检测单例模式中的差异
信息
- 类型:问答
- 难度:⭐⭐⭐
考点
Java内存模型, volatile关键字, synchronized原理, 指令重排序, 单例模式实现
快速回答
在双重检测单例模式中,volatile关键字解决的核心问题是指令重排序导致的初始化不完整对象可见性问题:
- 未使用
volatile时,对象初始化可能被重排序为:1.分配内存 2.引用赋值 3.初始化构造,导致其他线程获取到未初始化的实例 volatile通过内存屏障禁止指令重排序,保证写操作前的所有操作完成synchronized仅保证代码块内的原子性和可见性,无法防止重排序- 正确实现需同时使用
volatile和synchronized双重检测
问题背景
双重检测锁定(Double-Checked Locking)是实现线程安全单例模式的常见方式,但在Java 1.5之前存在严重缺陷:
// 有缺陷的实现
public class Singleton {
private static Singleton instance;
public static Singleton getInstance() {
if (instance == null) { // 第一次检测
synchronized (Singleton.class) {
if (instance == null) { // 第二次检测
instance = new Singleton(); // 问题根源
}
}
}
return instance;
}
}核心问题:指令重排序
对象初始化instance = new Singleton()包含三个步骤:
- 分配对象内存空间
- 初始化对象(执行构造函数)
- 将引用指向内存地址
JVM可能进行指令重排序(步骤2和3交换),导致其他线程获取到未初始化完成的对象。
解决方案:volatile关键字
修正后的实现:
public class Singleton {
private static volatile Singleton instance; // 添加volatile
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton(); // 安全初始化
}
}
}
return instance;
}
}volatile在此场景下的两大作用:
- 禁止指令重排序:通过内存屏障(Memory Barrier)确保写操作前的所有操作完成
- 保证可见性:写操作立即刷新到主内存,读操作从主内存获取最新值
内存屏障原理
JVM在volatile写操作前后插入屏障:
- 写前:StoreStore屏障(禁止普通写与volatile写重排序)
- 写后:StoreLoad屏障(禁止volatile写与后续操作重排序)
对象初始化过程变为:1.分配内存 → 2.初始化 → 3.写入volatile变量(触发屏障)
synchronized的局限性
虽然synchronized能:
- 保证代码块原子性
- 通过monitor exit自动刷新变量到主内存
但无法阻止初始化过程中的指令重排序,这是需要配合volatile的关键原因。
最佳实践
- Java 5+必须使用
volatile修复此问题(JMM增强) - 替代方案:静态内部类实现(利用类加载机制)
public class Singleton { private static class Holder { static final Singleton INSTANCE = new Singleton(); } public static Singleton getInstance() { return Holder.INSTANCE; // 懒加载且线程安全 } } - 枚举单例(Effective Java推荐)
public enum Singleton { INSTANCE; // 天然线程安全 }
常见错误
- 省略第二次null检查:导致每次访问都加锁
- 仅用
synchronized方法:性能低下 - 误认为
final字段能解决重排序(实际不能)
扩展知识
- Happens-Before原则:volatile写操作先于后续任意线程的读操作
- JMM内存可见性:线程栈、工作内存与主内存的交互模型
- Java 9+ VarHandle:更精细的内存控制替代方案