【揭秘】单例模式DCL导致无法访问对象?

前两天,在审查团队成员的代码时,我发现了一个错误的单例模式写法。

在Java中,单例模式是一种非常常见的设计模式,用于确保一个类只有一个实例,并提供一个全局访问点来获取该实例,但是,如果不正确地实现单例模式,就可能导致多个实例被创建,从而违反了单例模式的初衷。

如下,是你说的有问题的代码,即未使用volatile关键字的DCL单例模式实现:

java 复制代码
public class Singleton {  
    private static Singleton instance;  
  
    private Singleton() {}  
  
    public static Singleton getInstance() {  
        if (instance == null) { // 第一次检查  
            synchronized (Singleton.class) {  
                if (instance == null) { // 第二次检查  
                    instance = new Singleton(); // 创建实例  
                }  
            }  
        }  
        return instance;  
    }  
}

在这个有问题 的代码中,instance = new Singleton(); 这行代码实际上包含三个步骤:

  1. 分配内存给 Singleton 对象
  2. 调用 Singleton 的构造函数初始化对象。
  3. instance 字段指向新创建的对象。

在没有 volatile 关键字的情况下,由于指令重排序,步骤2和步骤3的顺序可能会被颠倒,这意味着,当其他线程看到 instance 不为null时(即步骤3已经完成),它可能会访问这个对象,但此时对象可能还没有被完全初始化(即步骤2还没有完成),这就你所说的"未初始化完全的实例对象"的问题。

为了避免这个问题,我们需要确保步骤2和步骤3之间的顺序不会被颠倒,即确保在使用双重检查锁定(DCL)实现单例模式时对象能够完全初始化并且不会被多个线程同时初始化,这就是为什么我们需要在 instance 字段上添加 volatile 关键字的原因,volatile 关键字能够确保变量的可见性和有序性,且保证变量的读写操作都是原子的和禁止指令重排序,从而保证了对象的完全初始化。

下面是使用volatile关键字修复后的DCL单例模式实现:

java 复制代码
public class Singleton {  
    private static volatile Singleton instance; // 声明为volatile,确保线程安全  
  
    private Singleton() {} // 私有构造函数,防止外部实例化  
  
    public static Singleton getInstance() {  
        if (instance == null) { // 第一次检查,如果为null才进入同步块  
            synchronized (Singleton.class) {  
                if (instance == null) { // 第二次检查,如果为null才创建实例  
                    instance = new Singleton(); // 创建实例对象  
                }  
            }  
        }  
        return instance; // 返回单例实例  
    }  
}

在这个修复后的实现中,当第一个线程执行到 instance = new Singleton(); 时,由于 instancevolatile 的,它会保证以下三件步骤按照顺序发生:

  1. 分配内存给 Singleton 对象。
  2. 调用 Singleton 的构造函数,完全初始化对象。
  3. instance 字段指向新创建的对象。

并且,由于 volatile 的内存屏障效应,这个初始化过程对其他线程是可见的,也就是说,其他线程在看到这个 instance 不为 null 时,能够保证它已经被完全初始化了,这样就避免了之前提到的"未初始化完全的实例对象"的问题。

该问题所涉及的核心知识点参考:

Java内存模型(JMM):JMM定义了线程和主内存之间的交互方式,每个线程都有自己的工作内存,线程之间共享主内存,同时,JMM规定了一些规则,确保变量的值在线程之间正确同步。

指令重排序 :编译器和处理器可能会对指令进行重排序,只要这种重排序在单线程环境下不改变程序的执行结果,它就是可接受 的,然而,在多线程环境下,这种重排序可能导致问题

完!

相关推荐
Tadas-Gao4 分钟前
大模型幻觉治理新范式:SCA与[PAUSE]注入技术的深度解析与创新设计
人工智能·深度学习·机器学习·架构·大模型·llm
PKUMOD4 分钟前
论文导读 | 在长上下文及复杂任务中的递归式语言模型架构
人工智能·语言模型·架构
大模型微调Online7 分钟前
深度复盘:Qwen3-4B-Instruct-2507微调实战——打造“快思考、强执行”的 ReAct IoT Agent
java·后端·struts
梦想的旅途223 分钟前
Python 开发企微第三方 API:RPA 模式下外部群主动调用实现
架构·企业微信·rpa
梦想的旅途237 分钟前
RPA 架构下的企微非官方 API:外部群主动调用的技术实现与优化
架构·企业微信·rpa
Z.风止1 小时前
Go-learning(1)
开发语言·笔记·后端·golang
光电大美美-见合八方中国芯1 小时前
【SOA仿真6】多层膜仿真计算
后端·restful
小马爱打代码1 小时前
Spring Boot:Sentinel 企业级熔断、降级与限流实战
spring boot·后端·sentinel
查无此人byebye1 小时前
阿里开源Wan2.2模型全面解析:MoE架构加持,电影级视频生成触手可及
人工智能·pytorch·python·深度学习·架构·开源·音视频
野犬寒鸦1 小时前
从零起步学习并发编程 || 第二章:多线程与死锁在项目中的应用示例
java·开发语言·数据库·后端·学习