作为GoF 23种设计模式之一的单例模式,在程序之中有大量的使用。所谓单例模式,简单说就是确保类在程序中只会被创建一个实例。看起来似乎很简单,那么下面这个样例符合基本需求吗?
public class Singleton { private static Singleton instance = new Singleton(); public static Singleton getInstance() { return instance; } }
是不是缺了点什么?原来,Java会自动为没有明确声明构造函数的类,定义一个public的无参构造函数。所以上面的例子并不能保证额外的对象实例被创建出来,别人完全可以直接“new Singleton()”。解决的方法十分简单,可以定义一个private的构造函数。
public class Singleton { private static Singleton instance = new Singleton(); private Singleton() {} public static Singleton getInstance() { return instance; } }
也有人建议声明为枚举,这是有争议的,我个人不建议选择相对复杂的枚举。毕竟日常开发不是学术研究。这样做除了有“炫技”的嫌疑外,看不出有什么明显的好处。
在这个专栏之前介绍HashMap的文章中,提到JDK标准类库中很多地方使用懒加载(lazy-load),改善初始内存开销,在单例的问题上也适用:
public class Singleton { private static Singleton instance = null; private Singleton() {} public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
这个实现在单线程环境下不存在问题,但如果处于并发场景下,就需要考虑线程安全。最简单就是将getInstance()标记为synchronized。
public class Singleton { private static Singleton instance = null; private Singleton() {} public static synchronized Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
事实上一旦对象实例被创建后,instance就不会被修改,如果每次调用getInstance()都要进入同步块,将导致性能低下。改进的方法是使用“双检锁”:
public class Singleton { private static volatile Singleton instance = null; private Singleton() {} public static Singleton getInstance() { if (instance == null) { // 尽量避免重复进入同步块 synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } }
这段实现的要点在于:
- 这里的volatile能够提供可见性,以及保证getInstance返回的是初始化完全的对象。也就是说,当Singleton类本身有多个成员变量时,需要保证初始化过程完成后,才能被get到。
- 在同步之前进行null检查,目的是尽量避免进入相对昂贵的同步块。
- 直接在class级别进行同步,保证线程安全的类方法调用。
在现在Java中,内存排序模型(JMM)已经非常完善,通过volatile的write或者read,能保证所谓的happen-before,也就是避免常被提到的指令重排。换句话说,构造对象的store指令能够被保证一定在volatile read之前。
当然,也有一些人推荐利用内部类持有静态对象的方式实现。其理论依据是对象初始化过程中隐含的初始化锁,这种和前面的双检锁实现都能够保证线程安全。不过语法稍显晦涩,未必有特别的优势。
public class Singleton { private Singleton() {} public static Singleton getInstance() { return Holder.instance; } private static class Holder { private static Singleton instance = new Singleton(); } }
可以看出,即使是看似简单的单例模式,在增加各种高标准需求之后,同样需要非常多的实现考量。
上面是比较学究的实现,其实实践中未必需要如此复杂。比如来自Java核心类库自己的单例实现java.lang.Runtime:
private static final Runtime currentRuntime = new Runtime(); private static Version version; // ... public static Runtime getRuntime() { return currentRuntime; } /** Don't let anyone else instantiate this class */ private Runtime() {}
- 它并没有使用复杂的双检锁之类。
- 静态实例被声明为final,这是被通常实践忽略的,一定程度保证了实例不被篡改(反射之类可以绕过私有访问限制),也有有限的保证执行顺序的语义。
【完】