懒加载单例模式中DCL方式和原理解析

一、DCL 是什么?

DCL(Double Check Lock,双重检查锁)是 Java 中懒加载单例模式的高性能实现方案,核心思路是:

  • 第一次检查:无锁判断实例是否已初始化,避免每次调用都加锁(提升性能);
  • 加锁:保证多线程下只有一个线程能进入初始化逻辑;
  • 第二次检查:防止多个线程等待锁后重复初始化实例。

它解决了传统 "懒汉式(同步方法)" 每次调用都加锁的性能问题,同时保证线程安全。

二、DCL 完整实现代码(标准写法)

java 复制代码
public class SingletonDCL {
    // 1. 必须加 volatile 关键字!
    private static volatile SingletonDCL INSTANCE;

    // 2. 私有构造器,禁止外部实例化
    private SingletonDCL() {}

    // 3. 双重检查锁的核心方法
    public static SingletonDCL getInstance() {
        // 第一次检查:无锁,快速判断实例是否已存在
        // 若已存在,直接返回,无需加锁,提升高并发性能
        if (INSTANCE == null) {
            // 加锁:保证同一时间只有一个线程能进入初始化逻辑
            synchronized (SingletonDCL.class) {
                // 第二次检查:防止多个线程等待锁后重复创建实例
                if (INSTANCE == null) {
                    // 初始化实例
                    INSTANCE = new SingletonDCL();
                }
            }
        }
        return INSTANCE;
    }
}

三、DCL 核心逻辑拆解(为什么要 "双重检查")

假设高并发场景下有 3 个线程(T1、T2、T3)同时调用 getInstance()

  1. T1 先执行 :第一次检查 INSTANCE == null 为 true,进入加锁逻辑,第二次检查仍为 true,执行 INSTANCE = new SingletonDCL(),初始化完成后释放锁;
  2. T2 随后执行 :第一次检查 INSTANCE 已不为 null,直接返回实例,无需加锁;
  3. T3 与 T1 同时执行 :T3 先通过第一次检查(此时 T1 还未完成初始化),等待 T1 释放锁后进入加锁逻辑,第二次检查发现 INSTANCE 已被 T1 初始化,直接返回,避免重复创建。

如果去掉 "第二次检查",T3 会在 T1 释放锁后重新创建实例,导致单例失效。

四、为什么必须加 volatile?(面试必考)

这是 DCL 最核心的坑点!INSTANCE = new SingletonDCL() 看似一行代码,实际 JVM 会拆分为 3 步执行:

java 复制代码
1. 分配内存空间(给 SingletonDCL 实例);
2. 初始化实例(执行构造器逻辑,给成员变量赋值);
3. 将 INSTANCE 引用指向分配的内存空间(此时 INSTANCE 不再为 null)。

JVM 为了优化性能,可能会对这 3 步进行指令重排(比如重排为 1→3→2),导致问题:

  • T1 执行时,JVM 先执行 1→3(INSTANCE 不为 null,但实例还未初始化);
  • T2 此时第一次检查 INSTANCE != null,直接返回这个 "半初始化" 的实例;
  • T2 调用实例的方法时,会因实例未初始化完成抛出空指针或逻辑错误。

volatile 关键字的核心作用:禁止 JVM 对指令重排 ,保证 1→2→3 的执行顺序,确保其他线程看到的 INSTANCE 要么是 null,要么是完全初始化的实例。

五、DCL 的常见误区(避坑)

错误写法 问题说明
去掉 volatile 可能拿到 "半初始化" 实例,线程安全失效
去掉第二次检查 多线程等待锁后重复创建实例,单例失效
同步代码块锁对象错误(比如锁 this static 方法中 this 不存在,且锁对象不唯一,线程安全失效
INSTANCE 定义为 final final 变量必须初始化,无法实现懒加载

六、DCL 的优缺点

优点 缺点
1. 懒加载:实例仅在第一次调用时初始化,节省内存; 2. 高性能:仅初始化时加锁,后续调用无锁; 3. 线程安全:双重检查 + volatile 保证单例唯一性 1. 实现稍复杂,新手易遗漏 volatile; 2. 无法解决 "反射 / 序列化破坏单例" 的问题(需额外处理); 3. 在早期 JDK(1.4 及之前)中,volatile 实现有缺陷,DCL 可能失效(现代 JDK 已修复)

七、DCL 的适用场景

  • 高并发场景下的懒加载单例(比如工具类、连接池、配置中心实例);
  • 对内存占用敏感,需要延迟初始化的场景;
  • 追求高性能,不希望每次调用都加锁的场景。

总结

  1. DCL 是 "懒加载 + 高性能 + 线程安全" 的单例实现方案,核心是 "两次检查 + 加锁 + volatile";
  2. volatile 是 DCL 的关键,用于禁止指令重排,避免拿到半初始化实例;
  3. 第二次检查不可省略,否则多线程下会重复创建实例;
  4. 现代 JDK(1.5+)中 DCL 是安全的,是生产环境中最常用的单例实现方式之一。
相关推荐
lee_curry8 小时前
第四章 jvm中的垃圾回收器
java·jvm·垃圾收集器
九转成圣9 小时前
Java 性能优化实战:如何将海量扁平数据高效转化为类目字典树?
java·开发语言·json
SmartRadio9 小时前
ESP32-S3 双模式切换实现:兼顾手机_路由器连接与WiFi长距离通信
开发语言·网络·智能手机·esp32·长距离wifi
laowangpython9 小时前
Rust 入门:GitHub 热门内存安全编程语言
开发语言·其他·rust·github
我叫汪枫9 小时前
在后台管理系统中,如何递归和选择保留的思路来过滤菜单
开发语言·javascript·node.js·ecmascript
_.Switch9 小时前
东方财富股票数据JS逆向:secids字段和AES加密实战
开发语言·前端·javascript·网络·爬虫·python·ecmascript
软件技术NINI9 小时前
webkit简介及工作流程
开发语言·前端·javascript·udp·ecmascript·webkit·yarn
Brendan_0019 小时前
JavaScript的Stomp.over
开发语言·javascript·ecmascript
念2349 小时前
f5 shape分析
开发语言·javascript·ecmascript
苍穹之跃9 小时前
某量JS逆向
开发语言·javascript·ecmascript