单例模式是属于比较简单,容易理解的设计模式,我们在课堂上都应该能轻易理解如何使用单例模式。
在系统运行时,有些类在系统中只需要存在一个实例即可。例如比较常见的是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; }
改进之后,只有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; } }