单例模式 - 终极篇
1. 前言
单例(Singleton)是设计模式当中使用比较常用和重要的一种模式,有些架构师并不把单例作为一种设计模式,而是一种实现方式。下面是我自己总结的7中单例模式的写法,废话不多说,直接上代码:(分享注明出处即可,看完这一篇基本上不用再看其他乱起八糟的总结了!)
2. 什么是单例?
单例对象的类必须保证只有一个实例存在(from wiki)
懒汉式:lazy load
第一种(懒汉式简单版):
public class Single1 {
private static Single1 instance;//此处一定是私有
// public Single1(){} 省略默认构造
public static Single1 getInstance(){
if (instance==null) {
instance=new Single1();
}
return instance;
}
}
很明显这是一种有缺陷的写法,初步优化,是将构造器私有,这样可以防止被外部的类调用。
1. //ViViD
2. public class Singleton {
3. private static Singleton instance = null;
4.
5. private Singleton() {
6. }
7.
8. public static Singleton getInstance() {
9. if (instance == null) {
10. instance = new Singleton();
11. }
12. return instance;
13. }
14. }
这种写法在大多数的时候也是没问题的。虽然具备lazyloding,但致命缺点:多线程下不能正常工作。这种写法能够在多线程中很好的工作,但是,遗憾的是,效率很低,99%情况下不需要同步。问题在于,当多线程工作的时候,如果有多个线程同时运行到if (instance == null)
,都判断为null,那么两个线程就各自会创建一个实例——这样一来,就不是单例了。
第二种(懒汉,线程安全):
1. public class Singleton {
2. private static Singleton instance = null;
3.
4. private Singleton() {
5. }
6.
7. public static Synchronized Singleton getInstance() {
8. if (instance == null) {
9. instance = new Singleton();
10. }
11. return instance;
12. }
13. }
这种写法能够在多线程中很好的工作,而且看起来它也具备很好的lazy loading,但是,遗憾的是,效率很低,99%情况下不需要同步。
加上synchronized关键字之后,getInstance方法就会锁上了。如果有两个线程(T1、T2)同时执行到这个方法时,会有其中一个线程T1获得同步锁,得以继续执行,而另一个线程T2则需要等待,当第T1执行完毕getInstance之后(完成了null判断、对象创建、获得返回值之后),T2线程才会执行执行。——所以这端代码也就避免了第二种中,可能出现因为多线程导致多个实例的情况。
但是,这种写法也有一个问题:给gitInstance方法加锁,虽然会避免了可能会出现的多个实例问题,但是会强制除T1之外的所有线程等待,实际上会对程序的执行效率造成负面影响。
第三种( Double-Check双重检验锁 )
Version2代码相对于Version1d代码的效率问题,其实是为了解决1%几率的问题,而使用了一个100%出现的防护盾。那有一个优化的思路,就是把100%出现的防护盾,也改为1%的几率出现,使之只出现在可能会导致多个实例出现的地方。 JDK1.5 以后源码里就是这样写的。——有没有这样的方法呢?当然是有的,改进后的代码Vsersion3如下:
1. // 带双重检测锁的单例
2. public class Singleton {
3. private static Singleton5 instance = null;
4.
5. private Singleton() {
6. }
7.
8. public static Singleton getInstance() {
9. if (instance == null) {
10. synchronized (Singleton.class) {
11. if (instance == null) {
12. instance = new Singleton();
13. }
14. }
15. }
16. return instance;
17. }
18. }
这个是第二种方式的升级版,俗称双重检查锁定,详细介绍请查看:JDK源码。
在JDK1.5之后,双重检查锁定才能够正常达到单例效果。
这个版本的代码看起来有点复杂,注意其中有两次if (instance == null)的判断,这个叫做『双重检查Double-Check』。
· 第一个if (instance == null),其实是为了解决Version2中的效率问题,只有instance为null的时候,才进入synchronized的代码段——大大减少了几率。
· 第二个if (instance == null),则是跟Version2一样,是为了防止可能出现多个实例的情况。
—— 这段代码看起来已经很完美了。
—— 当然,只是『看起来』,还是有小概率出现问题的。
这弄清楚为什么这里可能出现问题,首先,我们需要弄清楚几个概念:原子操作、指令重排。
饿汉式:eagerly load
饿汉式单例是指:指全局的单例实例在类装载时构建的实现方式。由于类装载的过程是由类加载器(ClassLoader)来执行的,这个过程也是由JVM来保证同步的,所以这种方式先天就有一个优势——能够免疫许多由多线程引起的问题。
第四种(饿汉,线程不安全):
1. public class Singleton {
2. //2、自定义一个本类对象。
3. private static Singleton1 intance = new Singleton();
4. //1、私有化构造函数。
5. private Singleton() {
6. }
7. //3、定义一个方法返回改对象。让其他程序通过这个方法就可以获取该对象。
8. public static Singleton getInstance() {
9. return intance;
10. }
11. }
第五种(静态内部类):
1. public class Singleton5 {
2. private Singleton5() {}
3.
4. private static class SingletonHolder {
5. private static final Singleton5 INSTANCE = new Singleton7();
6. }
7.
8. public static final Singleton5 getInstance() {
9. return SingletonHolder.INSTANCE;
10. }
11. }
这种方式同样利用了 classloder 的机制来保证初始化 instance 时只有一个线程,它跟第三种和第四种方式不同的是(很细微的差别):第三种和第四种方式是只要 Singleton 类被装载了,那么 instance 就会被实例化(没有达到 lazy loading 效果),而这种方式是 Singleton 类被装载了, instance 不一定被初始化。因为 SingletonHolder 类没有被主动使用,只有显示通过调用 getInstance 方法时,才会显示装载 SingletonHolder 类,从而实例化 instance 。想象一下,如果实例化 instance 很消耗资源,我想让他延迟加载,另外一方面,我不希望在 Singleton 类加载时就实例化,因为我不能确保 Singleton 类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化 instance 显然是不合适的。这个时候,这种方式相比第三和第四种方式就显得很合理。
第六种(枚举优雅版):
1. public enum Singleton {
2. INSTANCE;
3. public void whateverMethod() {
4. }
5. }
这是一个枚举类型……连class都不用了,极简。使用时可以直接Singleton.INSTANCE. whateverMethod();由于创建枚举实例的过程是线程安全的,所以这种写法也没有同步的问题。
这种方式是Effective Java作者Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。个人认为由于1.5中才加入enum特性,普及率不够高,在实际工作中,发现被使用的不是很广泛。
作者对这个方法的评价:
这种写法在功能上与共有域方法相近,但是它更简洁,无偿地提供了序列化机制,绝对防止对此实例化,即使是在面对复杂的序列化或者反射攻击的时候。虽然这中方法还没有广泛采用,但是单元素的枚举类型已经成为实现Singleton的最佳方法。
枚举单例这种方法问世一些,许多分析文章都称它是实现单例的最完美方法——写法超级简单,而且又能解决大部分的问题。
不过我个人认为这种方法虽然很优秀,但是它仍然不是完美的——比如,在需要继承的场景,它就不适用了。
第七种终极版 (volatile)
对于Double-Check这种可能出现的问题(当然这种概率已经非常小了,但毕竟还是有的嘛~),解决方案是:只需要给instance的声明加上volatile
关键字即可,volatile版本如下:
1. public class Singleton
2. {
3. private volatile static Singleton singleton = null;
4. private Singleton() { }
5. public static Singleton getInstance() {
6. if (singleton== null) {
7. synchronized (Singleton.class) {
8. if (singleton== null) {
9. singleton= new Singleton();
10. }
11. }
12. }
13. return singleton;
14. }
15. }
volatile
关键字的一个作用是禁止指令重排
,把instance声明为volatile
之后,对它的写操作就会有一个内存屏障
(什么是内存屏障?),这样,在它的赋值完成之前,就不用会调用读操作。
注意:volatile阻止的不singleton = newSingleton()这句话内部[1-2-3]的指令重排,而是保证了在一个写操作([1-2-3])完成之前,不会调用读操作(if (instance == null)
)。
——也就彻底防止了Version3中的问题发生。
——好了,现在彻底没什么问题了吧?
……
……
……
好了,别紧张,的确没问题了。大名鼎鼎的EventBus中,其入口方法EventBus.getDefault()
就是用这种方法来实现的