题目
如何设计线程安全的延迟初始化单例模式?
信息
- 类型:问答
- 难度:⭐⭐⭐
考点
类加载机制, 内存可见性, 指令重排序, 双重检查锁定, volatile关键字
快速回答
实现线程安全的延迟初始化单例需解决三个核心问题:
- 指令重排序问题:使用
volatile修饰实例变量 - 线程竞争问题:通过双重检查锁定(DCL)减少同步开销
- 类初始化安全性:利用静态内部类实现更优的方案
推荐两种实现方式:
- 双重检查锁定 +
volatile - 静态内部类(Holder模式)
问题核心挑战
延迟初始化单例需同时解决:
- 指令重排序:JVM 可能对构造函数和对象引用赋值进行重排序
- 内存可见性:一个线程初始化的对象对其他线程立即可见
- 线程竞争:避免多个线程同时创建实例
方案一:双重检查锁定(DCL)
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(); // volatile 禁止重排序
}
}
}
return instance;
}
}关键点解析:
volatile作用:禁止 JVM 对instance = new Singleton()的指令重排序(对象分配内存 → 初始化 → 引用赋值)- 双重检查:减少同步块进入次数,提升性能
- 常见错误:缺少
volatile可能导致线程获取到未初始化完成的对象
方案二:静态内部类(Holder模式)
public class Singleton {
private Singleton() {}
private static class Holder {
static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return Holder.INSTANCE; // 类加载时初始化
}
}原理说明:
- 利用 JVM 类加载机制:静态内部类
Holder在首次调用getInstance()时加载 - 类加载过程线程安全:JVM 保证
<clinit>()方法(类初始化)的同步 - 天然避免指令重排序问题:静态变量初始化在类加载时完成
最佳实践对比
| 方案 | 优点 | 缺点 |
|---|---|---|
| DCL + volatile | 支持延迟初始化,减少资源占用 | 代码稍复杂,需理解内存模型 |
| 静态内部类 | 代码简洁,无同步开销 | 无法传递初始化参数 |
扩展知识
- 枚举单例:
public enum Singleton { INSTANCE; }天然线程安全且防反射攻击 - 指令重排序证明:可通过 JITWatch 工具观察汇编代码(
putstatic先于invokespecial执行) - 其他线程安全方案:
AtomicReference的 CAS 操作,但实现更复杂
常见错误案例
// 错误版本:非线程安全
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton(); // 多线程可能创建多个实例
}
return instance;
}
// 错误版本:缺少 volatile
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton(); // 可能返回半初始化对象
}
}
}
return instance;
}