双重检查锁(DCL)与 volatile 的关键作用

深入理解单例模式:从饿汉式到双重检查锁(DCL)与 volatile 的关键作用

在 Java 开发中,单例模式(Singleton Pattern) 是最常用、也最容易被"写错"的设计模式之一。它确保一个类在整个 JVM 生命周期中只有一个实例 ,并提供全局访问点。然而,看似简单的单例,却在多线程环境下暗藏玄机。本文将带你从基础实现出发,深入剖析饿汉式、懒汉式、双重检查锁(DCL),并重点揭示 volatile 在 DCL 中不可替代的作用。


一、什么是单例模式?

单例模式的核心要求:

  1. 构造方法私有化 :防止外部通过 new 创建实例;
  2. 持有唯一静态实例 :通常为 private static 字段;
  3. 提供全局访问方法 :如 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 创建对象的过程可分解为:

  1. 分配内存(Memory Allocation);
  2. 初始化对象(调用构造方法,设置字段);
  3. 将引用指向内存地址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 后续的读操作,从而建立跨线程的内存屏障。

相关推荐
lee_curry4 小时前
第四章 jvm中的垃圾回收器
java·jvm·垃圾收集器
九转成圣5 小时前
Java 性能优化实战:如何将海量扁平数据高效转化为类目字典树?
java·开发语言·json
SmartRadio5 小时前
ESP32-S3 双模式切换实现:兼顾手机_路由器连接与WiFi长距离通信
开发语言·网络·智能手机·esp32·长距离wifi
laowangpython5 小时前
Rust 入门:GitHub 热门内存安全编程语言
开发语言·其他·rust·github
我叫汪枫5 小时前
在后台管理系统中,如何递归和选择保留的思路来过滤菜单
开发语言·javascript·node.js·ecmascript
_.Switch5 小时前
东方财富股票数据JS逆向:secids字段和AES加密实战
开发语言·前端·javascript·网络·爬虫·python·ecmascript
软件技术NINI5 小时前
webkit简介及工作流程
开发语言·前端·javascript·udp·ecmascript·webkit·yarn
Brendan_0015 小时前
JavaScript的Stomp.over
开发语言·javascript·ecmascript
念2345 小时前
f5 shape分析
开发语言·javascript·ecmascript
苍穹之跃5 小时前
某量JS逆向
开发语言·javascript·ecmascript