最简单的实现
在GoF的23种设计模式中,单例模式
是比较简单的一种。
所谓的单例模式,就是在整个应用中保证只有一个类的实例存在。例如一些线程池和数据库连接池都要求是单实例的。
一种最简单的实现就是把类的构造函数写成private
的,从而保证别的类不能实例化此类,然后在类中提供一个静态的实例并能够返回给使用者。
1 2 3 4 5 6 7 8 9 10 11 12 13
| public class SingletonClass { private static final SingletonClass instance = new SingletonClass(); public static SingletonClass getInstance(){ return instance; } private SingletonClass(){ } }
|
如上面所示,外部使用者如果需要使用SingletonClass
的实例,只能通过getInstance()
方法,并且它的构造方法是private
的,这样就保证了只能有一个对象存在。
延迟加载
上面的代码虽然简单,但是存在一个问题,无论这个类是否被使用,都会创建一个instance
对象。如果这个创建很耗时,比如需要连接10000次数据库,并且这个类还不一定被使用,那么这个创建过程就不是必须的。
为了解决这个问题,可以使用下面的方案:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public class SingletonClass { private static SingletonClass instance = null; public static SingletonClass getInstance(){ if(instance == null){ instance = new SingletonClass(); } return instance; } private SingletonClass(){ } }
|
如果要使用instance
的话,第一次调用getInstance()
发现instance
是null,然后就创建一个新的对象并返回;第二次再调用的时候,因为这个instance
已经不是null,因此不会再创建对象,直接将其返回。
这个过程就是延迟加载。
同步
上面的代码在单线程
中是可以正常运行的。但是在多线程中
就麻烦了。
如果线程A在第一次调用getInstance()
时判断instance
是null的,于是它开始创建实例(创建)。
就在这时,CPU发生了时间片轮转,线程B开始执行,它调用getInstance()
方法,同样检测到instance
是null的,这时B开始创建对象,创建完后切换到A继续执行,这时A会继续创建对象。
这样线程A和线程B各自拥有一个SingletonClass的对象。单例创建失败。
解决的方法也很简单,只需加锁就可以了。但是由于性能应该使用更细粒度的同步块而非同步方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| public class SingletonClass { private static SingletonClass instance = null; public static SingletonClass getInstance(){ synchronized (SingletonClass.class) { if(instance == null){ instance = new SingletonClass(); } } return instance; } private SingletonClass(){ } }
|
又是性能
上面已经用同步块代替同步方法减少锁的粒度来提高性能。
但是这样的修改还是不起太大作用,因为每一次调用getInstance()的时候必然要同步,性能问题还是存在。
但是,如果我们事先判断一下是不是为null再去同步呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| public class SingletonClass { private static SingletonClass instance = null; public static SingletonClass getInstance(){ if(instance == null){ synchronized (SingletonClass.class) { if(instance == null){ instance = new SingletonClass(); } } } return instance; } private SingletonClass(){ } }
|
这就是double-checkd locking设计实现的单例模式。
可见性
下面来想一下,创建一个变量需要哪些步骤呢?
一个是申请一块内存,调用构造方法进行初始化操作;另一个是分配一个指针指向这块内存。
这两个操作谁在前谁在后呢?JVM规范并没有规定。
那么就存在这么一种情况,JVM是先开辟出一块内存,然后把指针指向这块内存,最后调用构造方法进行初始化。
下面对程序进行分析:线程A开始创建SingletonClass实例,此时线程B调用了getInstance方法,首先判断instance是否为空,按照上面所说的内存模型,A已经把instance指向了那块内存,只是还没有调用构造方法,此时B检测到instance不为空,于是直接返回instance了。
问题出现了,尽管instance不为null,但是它并没有构造完成,此时如果B在A将instance构造完成之前就调用了这个实例,程序就会出现错误。
于是,我们想到了volatile变量:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| public class SingletonClass { private volatile static SingletonClass instance = null; public static SingletonClass getInstance(){ if(instance == null){ synchronized (SingletonClass.class) { if(instance == null){ instance = new SingletonClass(); } } } return instance; } private SingletonClass(){ } }
|
然而,这个只是java5之后的解决方法,因为java5之后才支持volatile,其实还有另外一种方法不会受到Java版本的影响:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public class SingletonClass { private static class SingletonClassInstance{ private static final SingletonClass instance = new SingletonClass(); } public static SingletonClass getInstance(){ return SingletonClassInstance.instance; } private SingletonClass(){ } }
|
在这个版本中,我们使用了Java的静态内部类。这种方式也是Effiective Java
推荐的。
至此,我们提出了两种完好的解决方案。