单例模式的实现
单例模式深度解析:从饿汉式到双重检查锁(DCL)的底层原理
作者:小饼干 | 2026年5月14日
单例模式作为 Java 设计模式中最基础也最常被问查的模式,核心目标很简单:确保一个类只有一个实例,并提供一个全局访问点。
但在多线程并发的环境下,想要写出一个既高效又安全的单例,其实有很多底层细节值得推敲。今天我们来聊聊单例的两种主流实现及其背后的内存模型。
一、 饿汉式(Eager Initialization)
饿汉式非常直观,它在类加载阶段就完成了实例的初始化。
1. 代码实现
public class SingletonEager {
// 类加载时就完成了初始化
private static final SingletonEager instance = new SingletonEager();
private SingletonEager() {}
public static SingletonEager getInstance() {
return instance;
}
}
2. 特点分析
- 优点:写法非常简单。由于实例在类加载时创建,利用了 JVM 的类加载机制来保证天然的线程安全,不需要额外的同步消耗。
- 缺点:不支持延迟加载。不管你程序中用不用这个实例,它都会提前创建好占用内存。如果这个对象很大或者初始化很重,可能会造成一定的内存浪费。
二、 懒汉式与双重检查锁(DCL)
为了解决内存占用问题,我们通常会选择“懒汉式”——即在第一次使用时才去创建实例。为了兼顾性能和线程安全,双重检查锁(Double-Check Locking, DCL) 成了标准写法。
1. 代码实现
public class Singleton {
// 必须配合 volatile 关键字使用
private volatile static Singleton uniqueInstance;
private Singleton() {}
public static Singleton getUniqueInstance() {
// 第一重判断:如果对象已创建,直接返回,避免不必要的加锁开销
if (uniqueInstance == null) {
// 类对象加锁
synchronized (Singleton.class) {
// 第二重判断:在捕获锁之后再次检查,防止阻塞在锁上的多个线程重复创建实例
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
2. 特点分析
- 优点:实现了延迟加载(Lazy Loading),仅在第一次调用时占用内存。通过
synchronized块缩小了锁的范围,性能优于直接在方法上加锁。 - 痛点:代码逻辑相对复杂,且必须通过
volatile来规避 指令重排 带来的潜在风险。
三、 深挖底层:为什么一定要加 volatile?
很多同学理解 DCL 时会忽略 uniqueInstance 前声明的 volatile。事实上,没有它,DCL 并不安全。
当我们执行 uniqueInstance = new Singleton(); 这行代码时,在汇编层面其实分成了 三步 执行:
- 分配空间:为
uniqueInstance分配内存空间。 - 初始化:执行构造函数,初始化对象内容。
- 赋值引用:将
uniqueInstance指向分配好的内存地址。
1. 指令重排的陷阱
由于 JVM 具有指令重排(Instruction Reordering)的特性,上述步骤的执行顺序可能从 1->2->3 变成 1->3->2。
在单线程下这没有问题,但在多线程环境下:
- 线程 T1 执行了步骤 1 和 3(此时对象已指向内存空间,但还未初始化)。
- 线程 T2 调用
getUniqueInstance(),执行第一重判断if (uniqueInstance == null)。 - 此时
uniqueInstance不为空,T2 直接返回了这个尚未初始化完成的对象。如果 T2 接着访问该对象的方法,就会报错。
2. Volatile 的救赎:内存屏障
加了 volatile 后,JMM(Java 内存模型)会插入特定的内存屏障来禁止这种重排序:
- StoreStore 屏障:在
volatile写(赋值引用)之前插入。保证步骤 1 和 2 的写入操作物理上先于步骤 3 完成。也就是说,引用的赋值绝对不会跑到初始化之前。 - StoreLoad 屏障:在
volatile写之后插入。保证步骤 3 的写入结果对其他线程立即可见。
有了这层屏障,当线程 T2 读取 uniqueInstance != null 时,我们可以 100% 保证该对象已经是一个完全初始化的可用对象了。
四、 总结
- 如果你追求简单省事,且对象占用资源不高,饿汉式是首选。
- 如果你追求极致性能和延迟加载,请务必使用 DCL + volatile。
这就是单例模式中的“细节决定成败”。作为 Java 开发者,不仅要会写代码,更要理解代码背后 JVM 和内存模型的运行逻辑。
希望这篇分享对你有帮助,我们下次再见!
版权声明:本文归“小饼干”所有,欢迎专业讨论。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)