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

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

相关推荐
贰先生1 小时前
Xiuno BBS 重构记录贴(十八)插件兼容扫描器
后端
神奇小汤圆1 小时前
阿里面试官:什么才是可工程化落地的RAG项目
后端
ZPYZTech1 小时前
用 Wails + Go + Vue3 开发桌面软件,聊聊踩过的坑
后端
好家伙VCC3 小时前
区块链双向支付通道实战:从签名到结算
java·后端·区块链·asp.net
我登哥MVP3 小时前
Spring Boot 从“会用”到“精通”:参数解析原理
java·spring boot·后端·spring·servlet·maven·intellij-idea
JustHappy4 小时前
古法编程秘籍(五):什么是进程和线程?从软件到 CPU 的一次完整旅程
前端·后端·代码规范
BLSxiaopanlaile4 小时前
关于常见 map的一些比较探究
后端
花大师4 小时前
基于深度学习的鼠标轨迹真实性检测系统
后端
小江的记录本5 小时前
【Spring全家桶】Spring Cloud 2023.0.x:微服务核心理论、CAP/BASE定理(附《思维导图》+《面试高频考点清单》)
java·spring boot·后端·spring·spring cloud·微服务·面试