MST

星途 面试题库

面试题:Java双重检查锁定单例模式与内存可见性

从Java内存模型的角度分析,在双重检查锁定单例模式中,为什么需要将单例实例的声明加上volatile关键字?如果不加会对单例模式的正确性产生什么影响?
36.9万 热度难度
编程语言Java

知识考点

AI 面试

面试题答案

一键面试

在双重检查锁定单例模式中,将单例实例声明加上volatile关键字主要有以下原因:

  1. 防止指令重排序:在Java内存模型中,为了提高性能,编译器和处理器会对指令进行重排序。在单例模式的初始化过程中,可能会出现以下指令重排序问题。假设单例类为Singleton,代码如下:
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指向分配的内存空间。

由于指令重排序,步骤2和步骤3可能会交换顺序。在多线程环境下,当线程A执行到instance = new Singleton();,如果发生了指令重排序,先执行了步骤3,此时instance已经不为null,但对象还未初始化完成。如果此时线程B进入getInstance方法,判断instance != null,就会直接返回一个未初始化完成的instance,导致程序出错。

而加上volatile关键字后,volatile关键字禁止了指令重排序优化,保证了步骤1、2、3的执行顺序,从而确保在多线程环境下单例对象的正确初始化。

  1. 保证可见性volatile关键字还能保证变量的可见性。当一个线程修改了volatile修饰的变量时,会立即将修改后的值刷新到主内存中,其他线程读取该变量时,会从主内存中获取最新的值。在单例模式中,若没有volatile修饰instance,当一个线程创建了单例对象并修改了instance的值后,其他线程可能无法及时看到这个修改,仍然认为instancenull,从而再次创建单例对象,破坏了单例模式的唯一性。

如果不加volatile关键字,在多线程环境下,可能会出现以下对单例模式正确性的影响:

  • 创建多个实例:由于指令重排序导致其他线程获取到未初始化完成的对象,从而再次创建实例,破坏单例模式的唯一性。
  • 使用未初始化完成的对象:线程获取到一个未完全初始化的单例对象,在使用该对象时可能会引发NullPointerException或其他未定义行为,导致程序运行错误。