今天来聊一聊单例模式,单例模式是我们平常使用较多的设计模式之一。刚开始接触单例模式的时候,觉得这个模式比较简单,容易理解,后来发现情况并不是这样。关于单例模式的争论也是比较多的,这些争论基本都是站在一个比较极端的角度来看单例模式,当然,从这些角度来看,单例模式也会有很多的问题,于是,人们开始解决这些问题,解决完他们说,单例模式是邪恶的。接下来我以几个争论点为起点,谈谈我世界里的单例模式。
1.懒汉模式中如何保证线程安全?
先来看看我们经常使用的懒汉模式,如下:
public class Singleton {
private static Singleton instance = null;
private Singleton(){};
public static Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
}
在执行getInstance方法时可能会有线程安全的问题,当两个线程并行的执行此方法,并且都判断了instance是否为空,这个时候,两个线程都会创建Singleton对象。是的,这里确实会有线程安全的问题,但是,我在开发中还是会经常用到上面的方法来实现单例,因为我可以确保第一次调用时不会有线程安全的问题。比如我经常用单例模式来管理项目的配置信息,在应用启动时,我会调用getInstance方法来获得一些基本的配置信息。记住,线程是否安全是由其使用场景决定的。
为了确保线程安全的问题,于是用上了锁:
public static synchronized Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
加了同步以后,安全了,但是同步的开销大了,效率上又不行了。于是,又开始了新一轮优化,其名美曰:双重检测。如下:
public static Singleton getInstance(){
if(instance == null){
synchronized (Singleton.class) {
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
双重检测的理论是完美的,不幸的是,现实测试是有问题的。再后来,牛逼的人们利用静态类让一切变的完美:
public class Singleton {
private Singleton(){};
private static class SingletonHolder {
static Singleton instance = new Singleton();
}
public static Singleton getInstance(){
return SingletonHolder.instance;
}
}
加载一个类时,其静态内部类不会同时被加载,只有当静态内部类的某个成员被调用时,他才会加载。当然,这个结论我们可以测试一下。
2.单例模式真的能保证单例么?
为了证明单例模式有问题,有人就发明了这么一种特殊情况:
// 1.正常方式获得对象
Singleton instance1 = Singleton.getInstance();
// 2.反射方式活得对象
Class cl = Class.forName("com.test.Singleton");
Constructor[] c = cl.getDeclaredConstructors();
c[0].setAccessible(true);
Singleton instance2 = (Singleton) c[0].newInstance();
单例?你看,我可以搞出”鸳鸯“,借用我高中老师的一句话:鸟大了,什么林子都有。有了反例,争论又开始了。有个人是这么反驳的:你每天单例单例,单例的是什么?单例用在哪里?用单例其实是想保存一些状态,我们只要保证状态是唯一的不就可以了?于是乎就有人用类的静态属性来保持状态,勉强的安慰了自己。用类的静态属性?那您请三思。
3.返璞归真
兜了一圈风,回到原点,我还没说最简单的单例模式的实现。把他放在最后,因为他是我们最推荐使用的。简单的就是最美的:
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {
}
public static Singleton getInstance() {
return instance;
}
}
为什么这么说了?懒汉模式为了延迟初始化设计的,如果单例类一直没有被使用,那么他就不会初始化,在这个角度上来说懒汉模式节约资源。但是回过头来想想,如果一直没有使用单例类,那可能是一种设计上的问题,也就是说,很多情况下,懒汉模式根本偷不了懒。所以一般情况下,我们就使用上面这个最简单的代码来实现单例。如果非要搞懒汗,我们上面也讨论了,可以用静态内部类来实现。至于第二个争论,用反射再来new出一个单例,我们姑且忽略,这种情况要么是思路的问题,要么是设计的问题,反正不是单例模式的问题。