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

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

完!

相关推荐
37手游后端团队几秒前
15页PPT深挖AI编程新范式!程序员必看的破局指南
程序员·ai编程·求职
钡铼技术ARM工业边缘计算机21 分钟前
TI AM62x异构处理器边缘计算网关重构储能 EMS 智能化管理新生态
后端
bobz96523 分钟前
compile libvirt
后端
星星电灯猴33 分钟前
iOS App安全实战:借助Ipa Guard提升应用抗逆向能力的开发者实用指南
后端
快起来别睡了36 分钟前
传统数据表创建与Prompt方式的对比:以NBA赛季投篮数据表设计为例
数据库·程序员
林鹿39 分钟前
Dart: 串联多个数据流
后端·架构·dart
异常君1 小时前
Java 应用中构建 Elasticsearch 多层次缓存:提升查询效率的实战方案
java·elasticsearch·架构
Java水解1 小时前
MySQL 分页查询优化
后端·mysql
想用offer打牌1 小时前
面试官拷打我线程池,我这样回答😗
java·后端·面试
用户6945295521701 小时前
国内开源版“Manus”——AiPy实测:让你的工作生活走上“智动”化
前端·后端