双重检查加锁(DCL)是Java中实现线程安全懒加载单例的经典方案,需配合volatile关键字防止指令重排序导致的半初始化问题。

在 Java 多线程环境中,双重检查加锁(Double-Checked Locking, DCL)是解决懒加载单例或高开销资源初始化时线程安全问题的经典方案。它不是“加一次锁就完事”,而是通过两次空值判断 + 一次同步块,把锁的粒度降到最低——只在真正需要创建实例时才加锁,且仅第一次调用才走锁路径。
为什么需要双重检查?
单次检查(比如只判 instance == null 再同步)不行:多个线程可能同时通过第一次判断,然后排队进同步块,但只有一个能创建实例,其余线程仍会执行 new Singleton() —— 这就破坏了单例。而单纯给整个方法加 synchronized 又太重,后续所有读操作都得排队。
双重检查的核心逻辑是:
- 先快速判断:实例已存在?→ 是,直接返回,不加锁
- 否,再进同步块;进块后再次判断:别人是否已经创建好了?→ 是,跳过创建;否,才真正初始化
必须加 volatile 关键字
即使写了双重检查,漏掉 volatile 依然可能出错。因为 new Singleton() 实际分三步:
- 分配内存空间
- 初始化对象(调用构造函数)
- 将引用赋值给静态变量
instance
JVM 可能对后两步重排序(即先赋值引用,再完成初始化)。这时另一个线程看到 instance != null,直接返回,却拿到一个“半初始化”的对象——引发 NullPointerException 或状态异常。加 volatile 能禁止这种重排序,并保证写操作对其他线程立即可见。
当用户要求为 Java 代码添加注释、修改注释、审查注释、整理注释格式,或要求按照代码注释规范处理 Java 文件时,使用本技能。本技能用于统一生成和维护 Java 类注释模板、按 Git 对比 release 判断版本号、补充每行代码上方中文块注释、调整注释空行规则,并保持 Java 文件 UTF-8 无 BOM 编码。典型触发场景包括:补充类注释、更新 @version、检查 @author 来源、整理行尾注释、把注释放到代码上一行、检查注释是否符合规范、构建前检查 Java 注释风格。
标准写法与关键细节
以下是安全、可直接复用的模板:
public class ResourceManager {
private static volatile ResourceManager instance;
private ResourceManager() {
// 防反射攻击(可选增强)
if (instance != null) {
throw new RuntimeException("Use getInstance() to get the instance.");
}
// 初始化耗时资源,如连接池、配置加载等
}
public static ResourceManager getInstance() {
if (instance == null) { // 第一次检查(无锁)
synchronized (ResourceManager.class) { // 加锁
if (instance == null) { // 第二次检查(有锁)
instance = new ResourceManager(); // 安全创建
}
}
}
return instance;
}
}
注意点:
-
synchronized锁的是类对象(ResourceManager.class),确保全局唯一锁 - 构造函数私有,且内部可加防反射逻辑(非必须,但推荐)
- 不要在
getInstance()中做复杂计算或 I/O,否则会拖慢同步块执行
它适合什么场景?
DCL 主要用于:资源创建成本高(如数据库连接池、大缓存加载)、启动时不确定是否用到、且必须保证全局唯一实例的场景。如果资源初始化很快,或确定程序启动就一定会用,用饿汉式更简单安全;如果追求极致健壮(防序列化/反射破坏),可考虑枚举单例。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南










