目录
三、Double-Checked Locking 的实现原理
一、模式定义与核心目标
Double-Checked Locking(双重检查锁定) 是一种用于在多线程环境下高效实现单例模式的设计模式。其核心目标是:
- 线程安全:确保单例实例在多线程环境下唯一创建。
- 性能优化:避免传统
synchronized
关键字对整个方法的锁竞争,仅在必要时加锁。
二、传统单例模式的缺陷
2.1 非线程安全的懒汉式(无锁)
public class Singleton {
private static Singleton instance;
private Singleton() {}
// 多线程下可能创建多个实例
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton(); // 非原子操作
}
return instance;
}
}
问题:
- 多线程并发时,可能多个线程同时通过
instance == null
检查,创建多个实例。
2.2 线程安全的懒汉式(全方法加锁)
public class Singleton {
private static Singleton instance;
private Singleton() {}
// 每次调用都加锁,性能低下
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
问题:
synchronized
修饰整个方法,即使实例已创建,后续调用仍需竞争锁,吞吐量低。
三、Double-Checked Locking 的实现原理
3.1 基础实现(JDK 1.5 前有缺陷)
public class Singleton {
private static Singleton instance; // 未声明 volatile
private Singleton() {}
public static Singleton getInstance() {
// 第一次检查:实例是否已创建,避免无意义的锁竞争
if (instance == null) {
synchronized (Singleton.class) { // 加锁范围缩小
// 第二次检查:防止多个线程同时通过第一次检查
if (instance == null) {
instance = new Singleton(); // 非原子操作,存在指令重排序问题
}
}
}
return instance;
}
}
关键逻辑:
- 第一次检查(
instance == null
):无锁状态下快速判断实例是否存在,避免大多数情况下的锁竞争。 - 加锁(
synchronized
):仅在实例未创建时对临界区加锁。 - 第二次检查:防止多个线程在等待锁时重复创建实例(如线程 A 释放锁后,线程 B 再次进入)。
3.2 指令重排序问题与 volatile
修正
问题根源:
instance = new Singleton()
并非原子操作,实际分为 3 步:
- 分配内存空间(
memory = allocate()
)。 - 初始化对象(
ctorInstance(memory)
)。 - 将
instance
指向分配的内存(instance = memory
)。
指令重排序可能导致步骤 2 和 3 顺序颠倒,即:
- 线程 A 执行到
instance = memory
(未完成初始化)。 - 线程 B 通过第一次检查(
instance != null
),直接返回未初始化的实例,导致 NPE 或逻辑错误。
修正方案:
在 JDK 1.5 后,通过 volatile
关键字禁止指令重排序:
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
作用:
- 可见性:确保各线程读取到
instance
的最新值。 - 有序性:通过 内存屏障(Memory Barrier) 禁止
instance
赋值与初始化操作的重排序。
四、模式的适用场景
4.1 单例模式的高性能实现
- 适用场景:
- 单例实例创建成本较高(如初始化耗时、资源占用大)。
- 系统中存在大量并发调用
getInstance()
的场景(如框架底层组件、配置管理器)。
- 典型案例:
- Spring 框架中
ApplicationContext
的单例实现(部分场景)。 - 数据库连接池、线程池的单例管理类。
- Spring 框架中
4.2 非单例场景的扩展应用
- 延迟初始化:
对任意类的实例化逻辑进行延迟加载,避免启动时创建所有对象:public class Resource { private volatile Data data; // 延迟加载的数据对象 public Data getResource() { if (data == null) { synchronized (this) { if (data == null) { data = loadDataFromDisk(); // 耗时操作 } } } return data; } }
五、优缺点分析
5.1 优点
- 线程安全:通过双重检查和锁机制确保实例唯一性。
- 高性能:仅在第一次创建实例时加锁,后续调用无锁竞争,吞吐量显著高于全方法加锁。
- 懒加载:实例在首次使用时创建,减少启动时间。
5.2 缺点
- 代码复杂度:双重检查逻辑增加了代码理解难度,需严格遵循
volatile
和锁的使用规范。 - 适用限制:
- 仅适用于无状态或线程安全的单例对象(若单例包含可变状态,需额外保证状态的线程安全)。
- 无法用于构造函数有副作用的场景(如构造函数中启动线程、注册监听器等)。
六、与其他单例模式的对比
单例模式实现方式 | 线程安全性 | 性能(多次调用) | 初始化时机 | 代码复杂度 |
---|---|---|---|---|
饿汉式(静态变量) | 安全 | 高(无锁) | 类加载时 | 简单 |
懒汉式(全方法加锁) | 安全 | 低(每次加锁) | 首次调用时 | 简单 |
Double-Checked Locking | 安全 | 高(仅首次加锁) | 首次调用时 | 复杂 |
枚举单例 | 安全 | 高 | 类加载时 | 极简 |
选择建议:
- 简单场景:优先使用枚举单例(线程安全、防反射攻击)或饿汉式。
- 高性能懒加载场景:使用 Double-Checked Locking(需注意
volatile
)。
七、常见错误与最佳实践
7.1 常见错误
- 遗漏
volatile
:
JDK 1.5 前的实现因指令重排序存在隐患,JDK 1.5 后若未声明volatile
,仍可能导致线程安全问题。 - 锁对象不一致:
错误使用非全局锁对象(如this
或成员变量),导致不同线程持有不同锁:// 错误示例:锁对象为实例而非类,未加锁时实例未创建,锁对象不存在 synchronized (instance) { ... }
7.2 最佳实践
- 优先使用静态内部类单例:
利用类加载机制实现线程安全的懒加载,避免显式锁和volatile
:public class Singleton { private Singleton() {} // 静态内部类持有单例实例,类加载时由 JVM 保证线程安全 private static class Holder { static final Singleton INSTANCE = new Singleton(); } public static Singleton getInstance() { return Holder.INSTANCE; } }
- 最小化锁范围:
确保synchronized
仅包裹必要的代码块,避免在锁内执行耗时操作(如 I/O、网络请求)。 - 防御反射攻击:
在构造函数中添加防反射逻辑(仅适用于单例场景):private Singleton() { if (instance != null) { throw new IllegalStateException("Singleton instance already initialized"); } }
八、总结
Double-Checked Locking 是多线程环境下实现高性能懒加载单例的经典模式,核心在于通过双重检查减少锁竞争和 **volatile
禁止指令重排序 **。其适用场景需满足:
- 单例实例创建成本高且需懒加载。
- 存在高并发调用需求。
实际开发中,若追求简单性和安全性,可优先选择枚举单例或静态内部类单例;若必须使用懒汉式且对性能敏感,则需严格遵循 volatile
和锁的规范,避免因指令重排序或锁使用不当导致的线程安全问题。