【揭秘】单例模式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规定了一些规则,确保变量的值在线程之间正确同步。

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

完!

相关推荐
Reart8 分钟前
Leetcode 188.买卖股票的最佳时机4(718)
后端·算法
金銀銅鐵16 分钟前
[Python] 为 Vole 机器语言实现图形化界面
python·程序员
Reart23 分钟前
Leetcode 123.买卖股票的最佳时期3(内有随心谈,718)
后端·算法
只与明月听1 小时前
LangChain 学习-掌握LangChain Core API
前端·人工智能·后端
无相求码1 小时前
数组越界为什么有时候不崩溃?VS2013 下栈上变量的幽灵布局解密
c语言·后端
momo1 小时前
智析AgentData-Analysis-Agent 0→1 架构、搭建与联调实战报告
架构
XUHUOJUN2 小时前
Azure Stack Hub 套餐、计划与订阅模型深度解析(上篇)
架构·azure stack
HONG````2 小时前
HarmonyOS ArkUI Image 组件完全指南:objectFit、占位图与加载回调
后端
Sam_Deep_Thinking3 小时前
DDD里的领域服务,到底什么时候该用?
程序员·架构·系统架构·ddd
一路向北North3 小时前
Spring Security OAuth2.0(25):分布式系统授权-拓展用户信息
java·后端·spring