前言
设计模式相关的推荐《敏捷软件开发 — 原则、模式与实践》作者 Robert C.Martin (Bob大叔)和《设计模式 — 可复用面向对象软件的基础》GOF 四人帮写的这两本书。前者对设计原则有详细描述,后者主要涉及经典的设计模式讲解。
单例模式
单例模式的特点:
- 构造函数需要私有化,不允许在外部对其进行实例化,整个域中只允许有一个实例存在。
单例模式又有三种常用的建立方法,如下所示。
饿汉式
程序猿乐趣多多啊,取名都这么逗比。从名字上面看就容易理解既然饿了就吃东西,那么意味着一开始就将实例初始化了,饿汉嘛,要先吃东西。代码如下:
/**
* 单例模式之饿汉模式
*/
publc class SingleTonOfEager {
// 刚开始就进行初始化
private static SingleTonOfEager sInstance = new SingleTonOfEager();
private SingleTonOfEager(){
// do nothing...
}
// 仅能靠getInstance()获取实例
public static SingleTonOfEager getInstance(){
return sInstance;
}
}
缺点
- 占用资源
懒汉式
懒人都是伸手党,要用的时候才伸手取东西,所以使用的时候开始实例化。
- 特点:需要使用同步锁,保证多线程情况下调用的安全性。
- 缺点:有了同步锁,虽然第一次已经实例化了,但是之后的调用依然有同步锁保证线程安全的,那么会导致资源不必要的消耗和同步开销,不建议使用该模式
/**
* 单例模式之懒汉模式
*/
publc class SingletonOfLazy {
private static SingletonOfLazy sInstance;
private SingletonOfLazy(){
// do nothing...
}
// 仅能靠getInstance()获取实例
public static synchronized SingletonOfLazy getInstance(){
// 需要用到的时候再初始化
if (sInstance == null) {
sInstance = new SingletonOfLazy();
}
return sInstance;
}
}
双重检查锁 DCL 单例
优点
- 需要时才进行初始化;
- 保证线程安全;
- 已经初始化后的实例被调用时不需要再次进行同步锁,保证资源的高利用率。
/**
* 单例模式之Double Check Lock模式
*/
public class SingletonOfDCL {
private static SingletonOfDCL sInstance;
private SingletonOfDCL(){
// do nothing...
}
public static SingletonOfDCL getInstance() {
if (sInstance == null) {
synchronized (SingletonOfDCL.class) {
if (sInstance == null) {
sInstance = new SingletonOfDCL();
}
}
}
return sInstance;
}
}
问题
- 为什么要用两次非空判断呢?
- 为什么有了 synchronized 关键字还需要 volatile 关键字呢?
为了保证不必要的同步判断,在 sInstance = new SingletonOfDCL()
这条语指令并不是原子指令,被编译器编译后会被编译成多条指令,大致做了 3 件事情:
- 为 SingletonOfDCL 实例分配内存;
- 调用 SingletonOfDCL 构造函数,初始化成员字段;
- 将 sInstance 对象指向分配的内存空间(此时 sInstance 就不是 null 了)。
问题 1:第一次判断为了避免每次都进行同步操作,synchronized 是重量级锁,每次都进行同步操作会影响性能;第二次判断是和 volatile 配合使用,由于 Java 中允许 CPU 乱序执行,对上面的执行顺序可能是 1-2-3,也可能是 1-3-2,那么多线程进行访问的时候,很可能某条线程在 2 之前先执行了 3,那么 sInstance 不为 null,其他线程对其进行访问时就会出现异常,而加上 volatile 的目的是为了防止虚拟机对其指令重排。因为 volatile 关键字有两个特性:
- 保证可见性;
- 防止指令重排;
缺点
- 可能会发生DCL失效
DCL模式是使用最多的模式。这种方式一般能满足需求
静态内部类单例模式
DCL有可能会失效,所以不建议使用 DCL,看到这里又惊呆了,DCL都不建议使用,所以衍生出了下面这种模式:
public class SingletonOfInnerClass {
private SingletonOfInnerClass(){}
public static SingletonOfInnerClass getInstance(){
return SingletonHolder.sInstance;
}
private static class SingletonHolder {
private static final SingletonOfInnerClass sInstance =
new SingletonOfInnerClass();
}
}
优点
- 只在需要使用时进行初始化;
- 初始化完成后不再进行二次初始化;
- 保证线程安全。
原理
这里利用到 JVM 中的 clinit
方法的特性,在进行类初始化的过程中会执行 clinit 方法将静态语句块和静态变量加载到运行时方法区,该过程如果有多个线程去初始化同一个类,那么只会有一个线程能够进行类的初始化,别的线程会处于阻塞状态。因此静态内部类实现的单例模式在被多线程调用的时候,是线程安全的。
参考
- 《Android 源码设计模式》
- 《深入理解 Java 虚拟机》
- 《Java 并发编程实战》