一直觉得单例模式是Gof 23种设计模式中最简单的,但这些天恰巧看到一些关于单例模式的书和文章,才意识到单例模式也可以挖掘出很多知识,而且可以开阔我们处理问题的思路。
实现单例模式我找到大致三种方法:通过公有静态final域实现、通过单元素枚举类型实现、通过静态工厂实现。
首先来简单看一下公有静态final域和单元素枚举实现方式,因为这两种方式都非常简洁且易于理解。最后着重分析我们使用最广泛的通过静态工厂单例模式的实现。
通过公有静态final域实现单例模式的代码是这样的简单:
public class Singleton
{
{
public static final Singleton INSTANCE = new Singleton();
private Singleton(){}
}
这种实现方式的缺点是欠缺灵活性。类直接将它的静态域提供给客户端,不能根据需要提供其它实例。如果类是通过方法提供给客户端实例,则方法返回的实例可以是其它实现(这要求该类有父类或实现接口),甚至可以改变其本质是否为一个真的单例而不需要改变其对外提供的API。而且现代的JVM通过内联静态工厂方法消除了静态工厂单例实现相比于公有静态final域实现的性能劣势。
单元素枚举类型是一个非常好的实现单例模式的方法,示例代码是这样的:
public enum Singleton
{
{
INSTANCE
public void otherMethod(){...}
}
这种方法可能还没有被广泛的采用,不过它是实现Singleton非常好的方式,我想不出这种方法有任何实质性的缺点。鼓励大家尝试应用这种实现方式。
现在来看一下我们最常用的通过静态工厂方法实现单例模式的情况。常用的形式有下面几种:
public class Singleton
{
{
private static Singleton instance = new Singleton();
private Singleton(){}
public static Singleton
getInstance() {
getInstance() {
return instance;
}
}
一般情况这种方式是很不错的,结构简洁,像我们预想的一样工作,不会出什么问题。但有一种思想也被广泛应用,那就是延迟初始化。
public class Singleton
{
{
private static Singleton instance;
private Singleton(){}
public static Singleton getInstance()
{
{
if(instance == null)
{
{
instance = new Singleton();
}
return instance;
}
}
延迟初始化的使用者想要在实例没有被真正引用前不执行初始化,只有实例真正被引用才让静态工厂方法去初始化单例类的实例。这种做法绝大多数情况都只是将初始化类的开销转移到了访问实例时,根据经验,这种优化往往是得不偿失的。更糟糕的情况出现在多线程的应用场景中。为了防止多线程同时实例化instance,就需要给工厂方法加上synchronized同步,线程同步即使在现在的JVM中仍然是相对昂贵的。每次调用该方法都将导致线程同步,显然是不合算的。
为了解决这个问题,可以只将实例化过程同步,代码如下:
private static Singleton instance;
public static Singleton
getInstance() {
getInstance() {
if(instance == null)
{
{
synchronized(Singleton.class)
{
{
instance = new Singleton();
}
}
return instance;
}
这种实现事实上并没有解决多线程可能实例化多个对象的问题。比如线程1和线程2同时通过if判断,虽然同步块阻止了两个线程同时创建对象,但这两个线程仍然会先后执行同步块中的代码,创建出两个对象。
一种叫做双重检测的技术被一些开发者应用来试图解决这个问题。
public static Singleton
getInstance() {
getInstance() {
if(instance == null)
{
{
synchronized(Singleton.class)
{
{
if(instance == null)
{
{
instance = new Singleton();
}
}
}
return instance;
}
但不幸的是这种复杂的检测机制仍然有可能失败,原因在于instance = new Singleton()并不是原子性的。在大部分的JVM实现中,这一语句被分成三步来完成:
1.为instance分配内存空间
2.在分配到的空间上执行构造方法
3.将instance指向该内在空间
又由于java的内存模型允许”无序写入“,所以2和3的顺序并不能保证,也就是说instance可能还没有被调用构造方法进行初始化就已经是非null的了。这样可能的结果就是第二个线程读取到这个不完整实例,并将其返回给客户端。情况变得更糟了!如果你执意要这么做,那就要给静态域加上volatile关键字,访问volatile变量就像变量自身处在一个同步块中一样。但要注意volatile的准确语意是在java1.5后才完整支持的。
哦,天啊,希望你不是如此的钟爱这种方法!我的建议是不到万不得已不要使用延迟初始化,如果在极少数情况下经过测试证明延迟初始化的确能带来大的性能优化,那么这里提供最后一种方式做到这一点。
public class Singleton
{
{
private Singleton(){}
private static class Builder
{
{
static final Singleton instance = new Singleton();
}
public static Singleton
getInstance() {
getInstance() {
return Builder.instance;
}
}
总而言之,用单元素枚举来实现单例模式吧,如果你实在觉得它怪异而采用静态工厂的方式,也尽量不要选用延迟加载。