前两天,在审查团队成员的代码时,我发现了一个错误的单例模式写法。
在Java中,单例模式是一种非常常见的设计模式,用于确保一个类只有一个实例,并提供一个全局访问点来获取该实例,但是,如果不正确地实现单例模式,就可能导致多个实例被创建,从而违反了单例模式的初衷。
如下,是你说的有问题的代码,即未使用volatile
关键字的DCL单例模式实现:
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();
这行代码实际上包含三个步骤:
- 分配内存给
Singleton
对象 - 调用
Singleton
的构造函数初始化对象。 - 将
instance
字段指向新创建的对象。
在没有 volatile
关键字的情况下,由于指令重排序,步骤2和步骤3的顺序可能会被颠倒,这意味着,当其他线程看到 instance
不为null时(即步骤3已经完成),它可能会访问这个对象,但此时对象可能还没有被完全初始化(即步骤2还没有完成),这就你所说的“未初始化完全的实例对象”的问题。
为了避免这个问题,我们需要确保步骤2和步骤3之间的顺序不会被颠倒,即确保在使用双重检查锁定(DCL)实现单例模式时对象能够完全初始化并且不会被多个线程同时初始化,这就是为什么我们需要在 instance
字段上添加 volatile
关键字的原因,volatile
关键字能够确保变量的可见性和有序性,且保证变量的读写操作都是原子的和禁止指令重排序,从而保证了对象的完全初始化。
下面是使用volatile
关键字修复后的DCL单例模式实现:
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();
时,由于 instance
是 volatile
的,它会保证以下三件步骤按照顺序发生:
- 分配内存给
Singleton
对象。 - 调用
Singleton
的构造函数,完全初始化对象。 - 将
instance
字段指向新创建的对象。
并且,由于 volatile
的内存屏障效应,这个初始化过程对其他线程是可见的,也就是说,其他线程在看到这个 instance
不为 null
时,能够保证它已经被完全初始化了,这样就避免了之前提到的“未初始化完全的实例对象”的问题。
该问题所涉及的核心知识点参考:
Java内存模型(JMM):JMM定义了线程和主内存之间的交互方式,每个线程都有自己的工作内存,线程之间共享主内存,同时,JMM规定了一些规则,确保变量的值在线程之间正确同步。
指令重排序:编译器和处理器可能会对指令进行重排序,只要这种重排序在单线程环境下不改变程序的执行结果,它就是可接受的,然而,在多线程环境下,这种重排序可能导致问题。
完!