java 单例模式中的双重检测为什么要加 volatile 关键字?

新手上路,请多包涵

Java 实现单例模式有方法有双重检测锁,代码如下:

public class Singleton {
    private static volatile Singleton singleton = null;

    private Singleton(){}

    public static Singleton getSingleton(){
        if(singleton == null){
            synchronized (Singleton.class){
                if(singleton == null){
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }    
}

我理解的 synchronized 关键字实现了可见性、原子性和有序性,临界区中的代码可以重排序,但是不能重排序到临界区外面,synchronized 实现的可见性是临界区中代码执行结束之后,里面的共享变量会刷新到主内存中,那么如果 new Singleton() 方法被拆成了三个操作,并且经过重排序之后的顺序是这样的话:

  1. 分配内存
  2. 将实例引用赋值给 singleton 变量
  3. 实例初始化

不管这三个操作怎么重排序,另外一个线程看到的结果都是这三个操作执行完成后的结果(因为 synchronized 的原子性),那不就相当于另外一个线程访问到的 singleton 如果不为 null 的话就肯定实例化了吗?为什么还要多此一举加个 volatile 关键字禁止重排序呢?

阅读 900
avatarAI BotBETA

在 Java 中,使用 volatile 关键字可以确保多线程间的可见性。

在上述示例中,如果 Singleton 类的构造函数被拆分成三个操作,即使 synchronized 关键字确保了原子性和可见性,但是由于 JVM 的内存模型,可能会存在重排序的情况。

考虑以下情况:

  1. 线程 A 进入临界区,singleton 为 null,线程 A 分配了内存,并将实例引用赋值给 singleton 变量。
  2. 线程 A 继续执行,实例初始化。
  3. 线程 A 退出临界区,将 singleton 的值刷新到主内存中。
  4. 线程 B 进入临界区,看到 singleton 不为 null,因此直接返回 singleton

在这个情况下,线程 B 没有看到 singleton 变量被赋值为新实例的操作,因此它获取的 singleton 可能不是最新的实例。

通过添加 volatile 关键字,可以禁止 JVM 进行重排序,确保了在多线程环境下,其他线程看到的 singleton 总是最新的实例。因此,双重检测锁加 volatile 关键字是为了确保在多线程环境下的可见性和禁止重排序。

2 个回答

我大概了解你的疑惑点,上面的评论其实已经可以解决你的疑惑了。
另外一个线程访问到的 singleton 如果不为 null 的话就肯定实例化了吗
问题的关键在于,Sychronized加锁的位置,它没有对getSingleton()整个方法解锁,而是判断singleton为null后才会去抢锁,所以多个线程可以同时进入getSingleton方法。
由于synchronized 的有序性是持有相同锁的两个同步块只能串行的进入,即被加锁的内容要按照顺序被多个线程执行,但是其内部的同步代码还是会发生重排序,使块与块之间有序可见。
那么如果不加上volatile防止指令的重排序,new Singleton() 方法被拆成了三个操作,并且经过重排序之后的顺序是这样的话:

  1. 分配内存
  2. 将实例引用赋值给 singleton 变量
  3. 实例初始化

其中线程A假设在sychronized块中将内存地址赋值给了对象,其他线程此时调用getSingleton(),发现singleton此时不为空了,那么直接返回singleton,但是此时singleton还未完成初始化,那么问题就出现了。

右上角单词:Singleton
uploaded_image.jpg

推荐问题
宣传栏