双重检查锁(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 后续的读操作,从而建立跨线程的内存屏障。

相关推荐
风流倜傥唐伯虎16 分钟前
Spring Boot Jar包生产级启停脚本
java·运维·spring boot
二十雨辰16 分钟前
[python]-AI大模型
开发语言·人工智能·python
Yvonne爱编码26 分钟前
JAVA数据结构 DAY6-栈和队列
java·开发语言·数据结构·python
Re.不晚27 分钟前
JAVA进阶之路——无奖问答挑战1
java·开发语言
你这个代码我看不懂34 分钟前
@ConditionalOnProperty不直接使用松绑定规则
java·开发语言
pas13642 分钟前
41-parse的实现原理&有限状态机
开发语言·前端·javascript
fuquxiaoguang1 小时前
深入浅出:使用MDC构建SpringBoot全链路请求追踪系统
java·spring boot·后端·调用链分析
琹箐1 小时前
最大堆和最小堆 实现思路
java·开发语言·算法
__WanG1 小时前
JavaTuples 库分析
java
坚持就完事了1 小时前
数据结构之树(Java实现)
java·算法