设计模式笔记-单例模式

好久没写博文,最近学习一些设计模式,顺便记录一下。 单实例Singleton设计模式可能是被讨论和使用的最广泛的一个设计模式了,这可能也是面试中问得最多的一个设计模式了。我们尝试从场景出发,来看看要怎么设计这个类。 img

场景

我们要得到一个类,整个系统中只能出现一个类的实例。这样的场景非常多,比如说一个国家,只有能有一个现任总统。仔细想想,要满足这一条件,我们觉得应该满足几个条件。

  1. 和大部分类不同,它的构造函数需要是私有的。否则,在任何地方大家都能够new这个实例,那么系统中就不能始终保持只存在一个实例的情况了。
  2. 既然没有公有构造函数,那么我如何实例化这个类呢?我们需要一个静态的方式让其形成实例,给个方法吧--getInstance(),这个方法要判断现在系统中有没有这个实例,如果有,则返回;如果没有则调用私有的构造方法来实例化这个类,并存好,等下次来getInstance()调用时返回。
  3. 所以我们还需要一个私有的变量来存储这个实例。
  4. 我们使用时就可以用**Singleton.getInstance()**得到它了。

根据这些条件我觉得我们可以得到朴素的教科书版本的代码:

基础版本(懒汉式,线程不安全)

1
2
3
4
5
6
7
8
9
10
11
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}

看上去很美好,解决了我们上诉的要求,满足懒加载(只有用到的时候才会去创建这个实例)。然而该方法有一个致命的弱点,当系统中几个线程同时调用这个方法时,就很有可能会实例化出多个实例来,也就是说线程不安全。为了解决这个问题最简单的方法就是加Synchronize关键字。代码如下:

懒汉式,线程安全

1
2
3
4
5
6
7
8
9
10
11
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}

好了,线程安全了,然而我们发现在每次调用getInstance()的方法时,我们都会上锁。但其实我们只需要在创建的时候上锁,而不创建的时候我们其实不需要上锁。如果在多线程的系统中有频繁的调用,那么这段代码的性能会比较低。那我们只在创建的时候加锁行不行?像这样:

懒汉式,线程不安全

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Singleton
{
private static Singleton singleton;
private Singleton(){}
public static Singleton getInstance() {
if (singleton== null) {
synchronized (Singleton.class) {
singleton= new Singleton();
}
}
return singleton;
}
}

看起来不错哦。应该没有问题了吧?!错!这还是有问题!为什么呢?前面已经说过,如果有多个线程同时通过(singleton== null)的条件检查(因为他们并行运行),虽然我们的synchronized方法会帮助我们同步所有的线程,让我们并行线程变成串行的一个一个去new,那不还是一样的吗?同样会出现很多实例。线程依然不安全,没有解决第一种方法的问题!!!!好了我知道了,这样,双重校验:

双重检验锁(double checked locking)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Singleton {
private volatile static Singleton instance; //声明成 volatile
private Singleton (){}
public static Singleton getSingleton() {
if (instance == null) { //Single Checked
synchronized (Singleton.class) {
if (instance == null) { //Double Checked
instance = new Singleton();
}
}
}
return instance ;
}
}

但是这种方法要对volatile关键字有想到深刻的理解,并且对Java的内存模型深度理解。同时这种方法在jdk1.5之前是不能有bug的。如果你在面试中使用了这种方法,但是又不能很好的解释这方法的话。面试官不会喜欢你。-,- 相信你不会喜欢这种复杂又隐含问题的方式,如果你仍有兴趣,请查看这里当然我们有更好的实现线程安全的单例模式的办法。

饿汉式 static final field法

这种方法非常简单,因为单例的实例被声明成 static 和 final 变量了,在第一次加载类到内存中时就会初始化,所以创建实例本身是线程安全的。

1
2
3
4
5
6
7
8
9
10
public class Singleton{
//类加载时就初始化
private static final Singleton instance = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return instance;
}
}

也就是说这种方法巧妙的避开了新建的过程,所以也不存在多线程调用时会产生的问题。大部分的情况下,这种方法能满足要求。但是吹毛求疵一下,这种方法在没用到它的时候它就已经实例化好了,它不是懒加载的形式。

静态内部类 static nested class

1
2
3
4
5
6
7
8
9
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}

老版《Effective Java》中推荐的方法,上面这种方式,仍然使用JVM本身机制保证了线程安全问题;由于 SingletonHolder 是私有的,除了 getInstance() 之外没有办法访问它,因此它只有在getInstance()被调用时才会真正创建;同时读取实例的时候不会进行同步,没有性能缺陷;也不依赖 JDK 版本。

最优雅版本--枚举 Enum

用枚举写单例实在太简单了!这也是它最大的优点。下面这段代码就是声明枚举实例的通常做法。

1
2
3
public enum EasySingleton{
INSTANCE;
}

居然用枚举!!看上去好牛逼,通过EasySingleton.INSTANCE来访问,这比调用getInstance()方法简单多了。

默认枚举实例的创建是线程安全的,所以不需要担心线程安全的问题。但是在枚举中的其他任何方法的线程安全由程序员自己负责。还有防止上面的通过反射机制调用私用构造器。

这个版本基本上消除了绝大多数的问题。代码也非常简单,实在无法不用。这也是新版的《Effective Java》中推荐的模式。

总结

小小的一个场景演化出了这么多方法。在一般的情况下饿汉式的方法用的比较多,在有懒加载要求时,静态内部类方法不错。

延伸阅读

请我喝杯咖啡吧!