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

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

完!

相关推荐
yz_518 Nemo16 分钟前
django的路由分发
后端·python·django
Stark、1 小时前
异常处理【C++提升】(基本思想,重要概念,异常处理的函数机制、异常机制,栈解旋......你想要的全都有)
c语言·开发语言·c++·后端·异常处理
逢生博客2 小时前
Rust 语言开发 ESP32C3 并在 Wokwi 电子模拟器上运行(esp-hal 非标准库、LCD1602、I2C)
开发语言·后端·嵌入式硬件·rust
神一样的老师3 小时前
构建5G-TSN测试平台:架构与挑战
5g·架构
椰椰椰耶3 小时前
【Spring】@RequestMapping、@RestController和Postman
java·后端·spring·mvc
huaqianzkh3 小时前
付费计量系统通用功能(13)
网络·安全·架构
2402_857583494 小时前
新闻推荐系统:Spring Boot的架构优势
数据库·spring boot·架构
2401_854391084 小时前
新闻推荐系统:Spring Boot与大数据
java·spring boot·后端
陈序缘4 小时前
Go语言实现长连接并发框架 - 任务管理器
linux·服务器·开发语言·后端·golang
bylander4 小时前
【AI学习】Mamba学习(一):总体架构
人工智能·深度学习·学习·架构