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() 方法被拆成了三个操作,并且经过重排序之后的顺序是这样的话:
- 分配内存
- 将实例引用赋值给 singleton 变量
- 实例初始化
不管这三个操作怎么重排序,另外一个线程看到的结果都是这三个操作执行完成后的结果(因为 synchronized 的原子性),那不就相当于另外一个线程访问到的 singleton 如果不为 null 的话就肯定实例化了吗?为什么还要多此一举加个 volatile 关键字禁止重排序呢?
在双重检测锁的单例模式实现中,将 singleton
变量标记为 volatile
是为了解决指令重排序问题。
指令重排序是指处理器在执行指令时可能会对指令进行重新排序,以提高执行效率。然而,由于多线程环境下的指令重排序可能导致单例对象的不正确初始化,因此需要使用 volatile
关键字来禁止重排序。
具体而言,在没有使用 volatile
的情况下,可能会发生以下情况:
- 线程A进入
getSingleton
方法中,发现singleton
为null,因此进入同步块中。 - 线程A在同步块中执行了分配内存、将实例引用赋值给
singleton
变量和实例初始化等操作,但是这些操作可能会被重排序。 - 在还没有完成实例初始化的情况下,线程B进入
getSingleton
方法,发现singleton
不为null(由于线程A的部分操作可能已经执行完成),于是直接返回singleton
。 - 线程B尝试使用
singleton
,但由于实例初始化的某些操作尚未完成,可能会导致错误。
通过将 singleton
声明为 volatile
,可以确保在每个线程中,所有的写操作(分配内存、初始化等)都发生在对 singleton
的读操作之前。这样就可以避免上述的问题。
综上所述,使用 volatile
关键字可以确保双重检测锁的单例模式在多线程环境下正确地实现延迟加载和单例对象的正确初始化。