题目
双重检查锁定实现单例模式在Java内存模型下的线程安全问题
信息
- 类型:问答
- 难度:⭐⭐⭐
考点
Java内存模型,volatile关键字,指令重排序,双重检查锁定,单例模式
快速回答
在Java中实现线程安全的单例模式时,双重检查锁定需要配合volatile关键字才能确保线程安全:
- volatile作用:防止指令重排序,确保对象初始化完成前不被其他线程访问
- 双重检查锁定:第一次检查避免不必要的同步,第二次检查确保单例唯一性
- 关键代码:
private static volatile Singleton instance; - 替代方案:使用静态内部类或枚举实现更简洁的线程安全单例
问题背景
在并发环境下实现单例模式时,双重检查锁定(Double-Checked Locking)是常见方案。但在Java内存模型(JMM)下,若未正确使用volatile关键字,会导致微妙的线程安全问题。
错误实现示例
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;
}
}Java内存模型下的问题
指令重排序风险:
对象初始化instance = new Singleton()包含三个步骤:
1. 分配内存空间
2. 初始化对象
3. 将引用指向内存地址
JVM可能重排序为1→3→2,导致其他线程获取到未初始化的对象。
可见性问题:
未使用volatile时,线程A的修改可能对线程B不可见,导致多次创建实例。
正确解决方案
public class Singleton {
// 关键:使用volatile禁止指令重排序
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关键机制
- 内存屏障:在写操作前后插入屏障,确保写操作先于读操作
- happens-before原则:保证
volatile写操作前的所有操作对后续读操作可见 - 禁止重排序:阻止JVM优化可能破坏初始化顺序的指令
替代实现方案
1. 静态内部类(推荐):
利用类加载机制保证线程安全
public class Singleton {
private static class Holder {
static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return Holder.INSTANCE;
}
}2. 枚举单例(最安全):
Java规范保证枚举实例创建的线程安全性和唯一性
public enum Singleton {
INSTANCE;
// 添加业务方法
public void doWork() { ... }
}最佳实践
- 优先选择静态内部类或枚举实现单例
- 必须使用双重检查锁定时,务必声明
volatile - 避免在构造函数中执行耗时操作
- 考虑序列化/反序列化对单例的影响
常见错误
- 忘记
volatile关键字(导致NPE或状态不一致) - 在
getInstance()方法上滥用synchronized(性能瓶颈) - 忽略序列化破坏单例的风险(需实现
readResolve()方法)
扩展知识
- JMM三大特性:原子性、可见性、有序性
- 内存屏障类型:LoadLoad、StoreStore、LoadStore、StoreLoad
- final字段的特殊处理:JVM保证final字段初始化安全(无需同步)
- VarHandle(Java9+):提供更细粒度的内存排序控制