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

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

相关推荐
愚农搬码8 小时前
2026 年开源表单设计器选型:哪个更好用?
后端
vipxieliang8 小时前
ValidX 正则表达式预编译优化:Pattern 复用策略
java·spring boot·后端
vipxieliang9 小时前
OpenTelemetry 生产实践:Skyscanner 24 集群管理经验与 Mastodon 小团队方案
后端
明月_清风9 小时前
为什么 Skill 很难商业化?Bot 才是 AI 时代真正的商业机会
人工智能·后端·agent
苍何9 小时前
DeepSeek 的 Super-agent 设计,和我们的工程实践
后端
挖掘狂人9 小时前
缓存和数据库的一致性,别停在 “延时双删”
java·后端·程序员
codigger9 小时前
缓存和数据库的一致性,别停在 “延时双删”
java·后端·程序员·后端开发
10年前端老司机9 小时前
Agent 商业化困局:热闹的发布会,冷清的付费客户
前端·后端·agent
小小张说故事9 小时前
Python 的 async def 为什么能同时跑几千个任务?拆开协程对象、await 与事件循环那次让出
后端·python
小小龙学IT9 小时前
Go 语言 pprof 性能剖析与调优深度解析:把“卡在哪“变成一张可执行的数据图
开发语言·后端·golang