设计模式之单例
定义
保证一个类仅有一个实例,并提供一个访问它的全局访问点。
简单来说,也就是需要保证一个类在整个应用程序的生命周期内,只能存在一个实例(没有也行)。为了达成这个目标,应该要做到以下几点:
- 私有的构造器,如果是构造器被声明为public的,则无法控制其实例化;
- 一个获取实例的公开方法,也就是定义中所说的『提供一个访问它的全局访问点』;
- 一个用来保持此唯一实例的静态变量。
实现方法
首先,我们按照前面说的三点,写了以下代码:
public class Singleton {
private Singleton instance;
private Singleton(){}
public Singleton GetInstance(){
return instance;
}
}
这样,显然是有问题的。
一是获取实例的公开方法getInstance,只是生命成public的,外部在没有Singleton的实例的情况下还是不能调用,就成了一个悖论了。所以需要把GetInstance方法声明为静态的。同样,由于需要被静态方法调用,同时还要用来保持唯一的实例,instance也需要声明为静态的。
二就是还缺少了对instance变量的初始化,即对构造器的调用。我们既可以再声明instance是就进行初始化,也可以在静态代码段中对它进行初始化。
于是乎,就有了下面两个版本的实现:
方法一:饿汉一
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton(){}
public static Singleton GetInstance(){
return instance;
}
}
方法二:饿汉二
public class Singleton {
private static Singleton instance;
static {
instance = new Singleton();
}
private Singleton(){}
public static Singleton GetInstance(){
return instance;
}
}
由于这两种方法没有实质上的区别,都是在类被加载的时候就进行了实例的初始化(所以被称为饿汉式)。这也是饿汉式的下的两个特点:
- 线程安全,因为在类加载时已经完成来实例化;
- 性能低,实例化后的对象在应用的声明周期中未必就会被使用,所以可能会产生计算的浪费。
方法三:懒汉
由于饿汉式在类加载时就完成了实例化,导致了可能存在性能浪费,所以我们就考虑看看能不能在类被使用时才被实例化呢。如果听说过『懒加载』这个的词话,应该就会觉得这很easy了,在之前的基础上,很轻松就写出了下面代码(懒汉式的单例)。
public class LazySingleton {
private static LazySingleton instance;
private LazySingleton() {}
public static LazySingleton GetInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
乍一看,已经是可以了,至少在单线程下已经没问题了。但是在多线程的场景下呢,很容易就会产生A、B两个线程同时调用GetInstance方法,A线程先判断instance为null,准备进行实例化,但在实例化之前B线程也进行了instance为null的判断,最终结果是两个线程分别调用了一次私有构造器进行实例化,第二次实例化的结果会将第一次的覆盖掉。
所以懒汉式有以下特点:
- 懒加载实现,第一次调用时实例化,不调用则不实例化,故计算效率高;
- 线程不安全,上面已经详细描述来是如何产生线程冲突的。
方法四:懒汉(线程安全)
为了解决上面方法的缺陷,也就是所谓的线程不安全,我们找到了synchronized这个关键字,也只加了这个关键字,得到下面的代码。
public class LazySingleton {
private static LazySingleton instance;
private LazySingleton() {}
public static synchronized LazySingleton GetInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
与前一种方法相比,在实现上只是在getInstance方法上增加了synchronized关键字,使得GetInstance方法同步,同一时间只能有一个线程执行这个方法,那我们之前说的线程不安全的问题显然就不在了,这样是不是就完美来呢?世界没那么美好,我们又引入了新的问题。
由于是在整个GetInstance方法上加锁(同步),但是因为实际上只需要进行一次实例化(也只允许进行一次),所以绝大多数场景下是不需要同步的,所以在并发场景下会导致效率降低,相当于多车道在这里并成单车道了。
方法五:懒汉(双重检查锁定)
既然还有问题,那我们就来继续进行优化。上面的方法因为把整个GetInstance方法设置为synchronized,所以导致多线程在这里受阻,那我们把同步的范围缩小一点儿,看看情况会不会好一些。
public class LazySingleton {
private static volatile LazySingleton instance;
private LazySingleton() {}
public static LazySingleton getInstance() {
if (instance == null) {
synchronized(LazySingleton.class){
if (instance == null) {
instance = new LazySingleton();
}
}
}
return instance;
}
}
优化之后,把锁定范围进行了收缩,只在需要进行初始化实例时才进行同步,之后就不再进行同步。
这样我们就得到了一种效率较高,并且线程安全的单例模式的构造方法。
PS. 如果有仔细看代码,您或许会发现我们在声明instance变量的时候,用了一个volatile关键字,如果需要一些解释的话,可以参考Java中的volatile在使用双层检查实现单例模式的解读,后面我也可能来单独说一下这个。
第六种:静态内部类
还有一种使用静态内部类来实现的单例,也被各种推荐。因为内部静态类是要在有引用了以后才会装载到内存的,这样就同样实现了懒加载;同时,静态内部类的静态变量的初始化,也是在被加载时进行的初始化,天然的完成来对进程安全的控制。
public class InnerStaticClassSingleton {
private Singleton() {}
private static class SingletonInstance {
private static final Singleton INSTANCE = new Singleton();
}
public static InnerStaticClassSingleton getInstance() {
return SingletonInstance.INSTANCE;
}
}
To Be Continued