双重检验锁的单例模式在高并发下的可见性问题

双重检查锁定(DCL) 单例模式中,如果没有使用 volatile 修饰实例变量,可能会因为指令重排序导致其他线程获取到未完全初始化的对象,从而引发可见性问题。

问题复现

java 复制代码
public class Singleton {
    private static Singleton instance;  // 缺少 volatile
    public static Singleton getInstance() {
        if (instance == null) {               // 第一次检查
            synchronized (Singleton.class) {
                if (instance == null) {       // 第二次检查
                    instance = new Singleton(); // 问题所在
                }
            }
        }
        return instance;
    }
}

instance = new Singleton(); 这一行在底层并非原子操作,它大致包含三个步骤:

  1. 分配内存 -- 为 Singleton 对象分配堆内存。
  2. 初始化对象 -- 调用构造器,对成员变量赋初值。
  3. 将引用指向内存地址 -- 让 instance 指向这块内存。

指令重排序的影响

在没有 volatile 保证有序性的情况下,编译器或 CPU 可能会将第 2 步(初始化)和第 3 步(引用赋值)颠倒顺序。于是实际执行顺序变为:

    1. 分配内存
    1. instance 指向内存地址(此时内存中的对象还未初始化)
    1. 初始化对象

可见性问题的发生过程

  1. 线程 A 进入 getInstance(),发现 instance == null,获得锁,执行 instance = new Singleton();
    由于重排序,A 先分配内存并让 instance 指向该地址(但尚未调用构造器初始化)。此时 A 可能被挂起或时间片用完。
  2. 线程 B 调用 getInstance(),第一次检查 instance
    instance 已经不为 null(因为 A 已经赋值了地址),于是 B 直接返回这个"半成品"对象。
  3. 线程 B 使用该对象,访问其成员变量时,由于对象尚未完成初始化,可能读取到默认值(如 0、null) 而非构造器中赋予的值,造成程序行为异常,甚至崩溃。

解决方案

使用 volatile 修饰 instance 变量 ,禁止指令重排序,确保 instance 引用的赋值发生在对象完全初始化之后。

java 复制代码
private static volatile Singleton instance;

结论

DCL 导致的可见性问题本质是指令重排序 使未完全初始化的对象对其他线程可见。通过 volatile内存屏障 来禁止这种重排序即可修复。在现代 Java 中,更推荐使用静态内部类枚举方式实现单例,它们更简洁且天然避免此类问题。

相关推荐
止语Lab1 小时前
好的 DX 不等于少写代码——三种语言的摩擦力设计课
后端
吃饱了得干活1 小时前
别再手动解析 LLM 输出了!LangChain 四种结构化输出方案对比
后端·python·langchain
程序员天天困1 小时前
Arthas trace 命令怎么用?一行定位最慢那行代码
jvm·后端
Huiturn1 小时前
GPT 5.6 连续编码 10 小时,纯 Python 啃下 Word 二进制格式——doc2docx 实现拆解
后端
用户298698530141 小时前
Python 实现 Excel 与 Markdown 互转的实用指南
后端·python·excel
用户77283104908401 小时前
krono-job:零侵入、单二进制交付的分布式任务调度平台(开源)
后端
Conan在掘金1 小时前
鸿蒙报错速查:Function lacks ending return statement,返回路径漏 return 就炸,根因 + 真解法
后端
人间凡尔赛2 小时前
AI-Native 云原生架构:2026 年从容器编排到智能体编排的范式革命
后端·云原生·架构
IT_陈寒2 小时前
Vue的响应式让我加班到凌晨3点,原来问题出在这
前端·人工智能·后端
卷无止境2 小时前
提升 Python 代码健壮性的方法大盘点
后端·python