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

双重检查锁定(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 中,更推荐使用静态内部类枚举方式实现单例,它们更简洁且天然避免此类问题。

相关推荐
东风破_2 小时前
从跨域到 WebSocket:前端跨域方案、SSE 与双向实时通信详解
前端·后端
行百里er4 小时前
Gateway 上别硬塞 MVC:DeferredImportSelector 看菜下锅
spring boot·后端·开源
名字还没想好☜4 小时前
Java 遍历时删元素抛 ConcurrentModificationException:fail-fast 原理与三种正确删法
后端
IT_陈寒5 小时前
React Hooks闭包陷阱差点让我加班到凌晨
前端·人工智能·后端
码事漫谈5 小时前
本体论,彻底懂
后端
创新技术阁5 小时前
FastapiAdmin 系统日志体系与核心配置参数详解
前端·后端·fastapi
掘金者阿豪5 小时前
SQL Server 迁到金仓,真正麻烦的不是数据,而是这些 T-SQL
后端
Kyrie_kk6 小时前
Java--IO--Path文件访问
java·后端
wxwx_bscxy3226 小时前
基于springboot宠物领养系统的设计与实现
数据库·spring boot·后端·spring·宠物
分支预测失败6 小时前
RISC-V AIA 中断架构实战:从 PLIC 到 APLIC 与 IMSIC 的迁移
linux·后端