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

相关推荐
爱学习的阿磊3 分钟前
C++中的策略模式应用
开发语言·c++·算法
nbsaas-boot3 分钟前
如何进行 Vibe Coding:从“灵感驱动”到“可交付工程”的方法论
java·ai编程
郝学胜-神的一滴4 分钟前
Python中的bisect模块:优雅处理有序序列的艺术
开发语言·数据结构·python·程序人生·算法
Remember_9936 分钟前
Spring 事务深度解析:实现方式、隔离级别与传播机制全攻略
java·开发语言·数据库·后端·spring·leetcode·oracle
看得见的风9 分钟前
Claude Code + CCR配置(含OpenRouter、GLM、Kimi Coding Plan)
开发语言
L_090710 分钟前
【Linux】进程状态
linux·开发语言·c++
roman_日积跬步-终至千里11 分钟前
【Java并发】用 JMM 与 Happens-Before 解决多线程可见性与有序性问题
java·开发语言·spring
空空kkk11 分钟前
SSM项目练习——hami音乐(三)
java·数据库
2401_8384725111 分钟前
C++异常处理最佳实践
开发语言·c++·算法
m0_7369191015 分钟前
C++中的类型标签分发
开发语言·c++·算法