Java双重校验单例模式详解


单例模式双重检测java实现:

public class Singleton { 
    private volatile static Singleton instance = null; //#1
    public static Singleton getInstance() { 
        if (instance == null) { //#2
            synchronized (SingletonClass.class) { //#3
                if(instance == null) { //#4
                    instance = new Singleton(); 
                } 
            } 
        } 
        return instance; 
    } 


#1.1:为什么要使用volatile?
通常来说在堆中创建变量,会有两个步骤:

  • 在堆中分配内存空间、执行初始化(就是new做得事情)
  • 在栈中的本地变量表分配一个指向该内存区域的reference(等于号做得事情)

但JVM会进行编译优化,并不一定按照这样的顺序执行。在多线程环境下,若线程A创建instance,首先分配了reference的指针,此时线程B并发地去执行getInstance方法,那么会发现instance所指向的内存区域并不是null,那么线程B的getInstance方法则会返回这个instance,但实际上线程A仅仅是分配了这个指针,并没有在内存区域中完成初始化方法。

volatile禁止了JVM对指令顺序的优化,使得创建变量严格按照先分配内存再分配指针的顺序执行。

同时,如果没有volatile,某线程修改变量,并不会立即存入共享内存(主存),而是存入线程私有的一块高速缓存区域(在CPU中的cache),而volatile要求线程对变量修改完之后立即存入共享内存,保证了变量修改的可见性。

#1.2:为什么要static?

保证单例对象是属于类的,而不是属于对象实例的,保证一个类只有一个变量。

#3 为什么不给getInstance方法修饰synchronized,而是在这里给类上锁?

因为没有必要,如果在最外层判断出已有单例对象,则无需调用任何同步方法。而若给getInstance方法修饰synchronized,那么无论如何都有synchronized带来的额外开销(即便synchronize进行了大量优化)。

#4 为什么需要第二层检验?

在多线程环境下,若没有语句#4,想象这样的场景:

  1. 线程A执行完#2
  2. 在#2、#3之间发生了线程切换,切换到线程B
  3. 线程B执行#3,获取到了锁,并进行instance初始化
  4. 切换回线程A,线程A执行#3,获取到了锁,并进行instance初始化

会发现instance被初始化了两次,因此必须进行第二层检验。