单例模式
Table of Contents

单例模式是属于比较简单,容易理解的设计模式,我们在课堂上都应该能轻易理解如何使用单例模式。

什么是单例模式

在系统运行时,有些类在系统中只需要存在一个实例即可。例如比较常见的是window的回收站,整个系统只需要维护一个回收站就可以了。单例模式的好处是节省了不必要的资源开销,只需要维护一份实例就可以了。

最常见的单例模式

这种方式创建的单例模式是线程不安全的,当两个线程并发调用获取实例的方法时,可能会new两次实例。破坏了单例。

public class Single {
    private static Single single;

    //隐藏构造方法
    private Single(){}

    public Single getInstance(){
        if (single == null){
            single = new Single();
        }
        return single;
    }
}

改进版,线程安全

在获取实例的方法前加了同步关键字,确保了并发环境中只能有一个线程访问这个方法。但是加上了同步关键字之后性能会降低很多。

public synchronized Single getInstance(){
    if (single == null){
         single = new Single();
    }
    return single;
}

改进版+1,双重校验锁

改进之后,只有single为空的时候才会进入同步方法里面新建实例。

    public Single getInstance(){
        if (single == null){
            synchronized (Single.class) {
                if (single == null){
                    single = new Single();
                }
            }
        }
        return single;
    }

这段代码看起来很完美,很可惜,它是有问题。主要在于single=new Single()这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情。
1、给 single 分配内存
2、调用 Single的构造函数来初始化成员变量
3、将single对象指向分配的内存空间(执行完这步 single 就为非 null 了)
但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 single 已经是非 null 了(但却没有初始化),所以线程二会直接返回 single,然后使用,然后顺理成章地报错。

所以最终的解决方案是利用volatile来使得指令顺序不排序。

private volatile static Single single;

使用静态内部类的方式实现单例模式

这是在《Java性能优化》这本书中看到的。
通过JVM来实现线程安全的单例模式,原理是类加载时,其内部类不会初始化,而当调用到内部类时,内部类才会进行初始化。

public class Singleton {  
    private Singleton() {}  

    static class SingletonHolder {  
        private static final Singleton instance = new Singleton();  
    }  

    public static Singleton getInstance() {  
        return SingletonHolder.instance;  
    }  
}