深入了解单例模式

最简单的实现


在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推荐的。

至此,我们提出了两种完好的解决方案。