单例模式应该是23种设计模式中最容易理解,也是最容易手写代码的模式了。但是要正确写出单例模式确是对基本功的极大考验。因为其中的坑却不少,所以也常作为面试题来考。本文由浅入深教你写出正确优雅的单例模式代码,带你装逼带你飞。

写法1:简单粗暴

public class Singleton {
    private static Singleton instance;
    private Singleton (){}

    public static Singleton getInstance() {
     if (instance == null) {
         instance = new Singleton();
     }
     return instance;
    }
}

此写法看起来十分赏心悦目,然而却有十分重大的漏洞:在多线程环境下并非线程安全的:如果多个线程同时调用getInstance(),便会创建出多个Singleton实例,这便违背了单例的初衷。

写法2:兵来将挡

public static synchronized Singleton getInstance() {
    if (instance == null) {
        instance = new Singleton();
    }
    return instance;
}

既然写法1存在多线程非安全问题,那么好办,最直接的解决办法就是使用同步来对接口访问进行串行化。这在语义上一定是正确的,然而,这在性能上却有大问题:多线程访问时必须要一个个穿行处理才能获得正确的语义。但是实际上呢,我们只要在第一次:也即instance==null时同步一下就可以了,在instance被创建好以后,对接口的访问就无需串行化了。

写法3:更上一层楼

public static Singleton getSingleton() {
    if (instance == null) {                //Single Checked
        synchronized (Singleton.class) {
            if (instance == null) {        //Double Checked
                instance = new Singleton();
            }
        }
    }
    return instance ;
}

有了上面写法2的分析,写法3看起来便顺理成章了:第一次==null检查如果通过,在创建之前再进行一次==null检查,这是为了避免线程B获取锁后重复创建。这样,在instance被创建好以后,后续的getInstance()调用其实是无需在同步语义的保护下进行,相比2,效率提升了不少。

然而,这种写法却存在细微漏洞,原因在于instance = new Singleton()这句,这并非是一个原子操作,事实上,在JVM中这句话大概做了3件事:

  1. 给instance分配内存;
  2. 调用Singleton的构造函数来初始化成员变量;
  3. 将instance对象指向分配的内存空间(完成这步后instance就为非null了)

但在JVM的即时编译器中存在指令优化重排序的可能:1-2-3的执行顺序可能被优化成1-3-2。这就引发了问题:在执行1-3后,尚未执行2前,注意此时其实instance已经为非null了,如果线程B访问接口getInstance(),于是返回了未初始化完成的instance,接下来的动作便可能出错。

写法4:查缺补漏

public class Singleton {
    private volatile static Singleton instance; //声明成 volatile
    private Singleton (){}

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

解决3的写法漏洞方式也很简单:将instance声明为volatile,java中的volatile关键字有禁止指令重排序优化的功能:在volatile变量赋值操作后面会有一个内存屏障,读操作不会被重排序到内存屏障之前。在本例中,instance = new Singleton()执行成功后,后续的读才能看到,即:只有初始化成功的instance才会被接下来读取到,解决了3中存在的问题。

写法5:返璞归真

public class Singleton{
    // 类加载时就初始化
    private static final Singleton instance = new Singleton();
    private Singleton(){}
    public static Singleton getInstance(){
        return instance;
    }
}

这种写法非常简单:因为单例的实例被声明成static final,因此在类加载时变初始化好,一定是单线程。这就巧妙地避免了多线程访问时的同步问题。但这种方式的缺陷在于无法做到延迟加载。

写法6:大器晚成

public class Singleton {  
    private static class SingletonHolder {  
        private static final Singleton INSTANCE = new Singleton();  
    }  
    private Singleton (){}  
    public static final Singleton getInstance() {  
        return SingletonHolder.INSTANCE; 
    }  
}

这种写法同样利用了JVM本身的机制来保证线程安全机制:由于SingletonHolder是Singleton的内部类,只有Singleton才能访问它,而且只有在getInstance()时该类才会被加载,满足了延迟加载的特性。这也是 <>的推荐写法。

写法7:天外飞仙

public enum EasySingleton{
    INSTANCE;
}

我们可以通过EasySingleton.INSTANCE来访问实例,这比上面任何一种方式都简单。java中创建枚举是线程安全的。