深入理解单例模式:从饿汉式到双重检查锁(DCL)与 volatile 的关键作用
在 Java 开发中,单例模式(Singleton Pattern) 是最常用、也最容易被"写错"的设计模式之一。它确保一个类在整个 JVM 生命周期中只有一个实例 ,并提供全局访问点。然而,看似简单的单例,却在多线程环境下暗藏玄机。本文将带你从基础实现出发,深入剖析饿汉式、懒汉式、双重检查锁(DCL),并重点揭示 volatile 在 DCL 中不可替代的作用。
一、什么是单例模式?
单例模式的核心要求:
- 构造方法私有化 :防止外部通过
new创建实例; - 持有唯一静态实例 :通常为
private static字段; - 提供全局访问方法 :如
getInstance()。
java
public class Singleton {
private static Singleton instance;
private Singleton() {} // 私有构造
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
⚠️ 注意:上述代码是线程不安全的懒汉式,仅作示意!
二、饿汉式 vs 懒汉式:两种经典实现
1. 饿汉式(Eager Initialization)
实现:在类加载时就创建实例。
java
public class EagerSingleton {
// 类加载时即初始化
private static final EagerSingleton INSTANCE = new EagerSingleton();
private EagerSingleton() {}
public static E s getInst ance() {
return INSTANCE;
}
}
✅ 优点:
- 线程安全:JVM 保证类加载过程的线程安全性;
- 实现简单 :无同步开销,
getInstance()性能极高。
❌ 缺点:
- 资源浪费:即使从未使用该实例,也会在类加载时创建;
- 不支持延迟加载:无法在需要时才初始化(如依赖配置文件、数据库连接等)。
📌 适用场景:实例占用资源小、初始化成本低、一定会被使用的场景。
2. 懒汉式(Lazy Initialization)
实现 :在第一次调用 getInstance() 时才创建实例。
java
public class LazySingleton {
private static LazySingleton instance;
private LazySingleton() {}
// ❌ 线程不安全!
public static LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
✅ 优点:
- 延迟加载:节省内存,按需创建;
- 资源友好:适合重量级对象(如数据库连接池)。
❌ 缺点:
- 线程不安全 :多线程并发调用
getInstance()时,可能创建多个实例!
🔥 线程不安全示例:
Thread A: if (instance == null) → true
Thread B: if (instance == null) → true
Thread A: instance = new LazySingleton(); // 实例1
Thread B: instance = new LazySingleton(); // 实例2 → 单例破坏!
💡 如何修复?加锁!但简单加锁会带来性能问题。
三、双重检查锁(Double-Checked Locking, DCL)
为兼顾线程安全 与性能,DCL 应运而生:
java
public class DCLSingleton {
private static DCLSingleton instance;
private DCLSingleton() {}
public static DCLSingleton getInstance() {
if (instance == null) { // 第一次检查(无锁)
synchronized (DCLSingleton.class) {
if (instance == null) { // 第二次检查(有锁)
instance = new DCLSingleton();
}
}
}
return instance;
}
}
✅ DCL 的优势:
- 减少锁竞争:只有首次创建时加锁,后续调用直接返回(无锁);
- 延迟加载:满足懒汉式需求;
- 线程安全 :通过
synchronized保证实例创建的原子性。
⚠️ 但是!DCL 在 Java 5 之前存在致命缺陷 ------ 指令重排序(Instruction Reordering)。
四、volatile:禁止重排序的关键
问题根源:new Singleton() 不是原子操作!
JVM 创建对象的过程可分解为:
- 分配内存(Memory Allocation);
- 初始化对象(调用构造方法,设置字段);
- 将引用指向内存地址 (
instance = memory_address)。
但在 未使用 volatile 时,JVM 可能进行指令重排序,执行顺序变为:
1 → 3 → 2
即:先赋值引用,再初始化对象!
🌰 重排序导致的 DCL 失败:
Thread A: 执行 new DCLSingleton()
- 分配内存
- 赋值 instance = 内存地址(此时对象未初始化!)
Thread B: 调用 getInstance()
- 第一次检查:instance != null → 直接返回!
- 使用未初始化的对象 → NullPointerException 或数据错误!
💥 这就是著名的 "部分初始化对象"(Partially Constructed Object) 问题。
五、DCL 中的对象必须用 volatile 修饰!
✅ 正确的 DCL 实现:
java
public class SafeDCLSingleton {
// 关键:volatile 禁止重排序 + 保证可见性
private static volatile SafeDCLSingleton instance;
private SafeDCLSingleton() {}
public static SafeDCLSingleton getInstance() {
if (instance == null) {
synchronized (SafeDCLSingleton.class) {
if (instance == null) {
instance = new SafeDCLSingleton(); // volatile 确保完整初始化
}
}
}
return instance;
}
}
🔑 volatile 的两大作用:
| 作用 | 说明 |
|---|---|
| 禁止指令重排序 | 确保 new Singleton() 的三步操作按 1→2→3 顺序执行; |
| 保证可见性 | Thread A 初始化完成后,Thread B 能立即看到最新值(而非缓存旧值)。 |
📜 Java 内存模型(JMM)规定 :
对
volatile变量的写操作 happens-before 后续的读操作,从而建立跨线程的内存屏障。