单例设计模式总结
单例模式简介
单例模式是应用最广泛的模式之一,在应用这个模式时,单例对象的类必须确保只有一个实例存在,避免产生多个对象消耗过多的资源。如在访问IO 和数据库时可以考虑使用单例模式。
单例模式的 UML 类图
Client - 高层客户端;
Singleton - 单例类;
实现单例模式的关键点
1). 构造函数私有化;
2). 通过一个静态方法或枚举返回单例类对象;
3). 确保单列类对象有且只有一个,尤其是在多线程环境下;
4). 确保单列类对象在反序列化是不会重新构建对象;
几种单列模式
1- 懒汉模式
public class Singleton { private static Singleton sInstance; private Singleton() { } public static synchronized Singleton getInstance() { if (sInstance == null) { sInstance = new Singleton(); } return sInstance; } }
优点:只有在使用时才会被实例化,在一定程度上节约了资源;
缺点:第一次加载时需要及时实例化,每次调用 getInstance() 方法都进行同步,造成不必要的同步开销。
这种模式一般不建议使用。
2- 饿汉模式
public class Singleton { private static Singleton sInstance = new Singleton(); private Singleton() {} public static synchronized Singleton getInstance() { return sInstance; } }
优点:类加载时实例化,避免了线程不同步问题;
缺点:无法对singleton实例做延迟加载。
3 - Double Check Lock(DCL) 模式
public class Singleton { // private static Singleton sInstance = null; //JDK 1.5 以后写法 private static volatile Singleton sInstance = null; private Singleton() {} public static Singleton getInstance() { if (sInstance == null) { synchronized (Singleton.class) { if (sInstance == null) { sInstance = new Singleton(); } } } return sInstance; } }
为什么 JDK1.5以后版本改变了写法呢?
这是因为 sInstance = new Singleton() 语句并非是一个原子操作,这句代码最终会被编译成多条汇编指令,大致做了3件事情:
(1)给 Singleton 的实例分配内存;
(2)调用 Singleton() 的构造函数,初始化成员字段;
(3)将 sInstance 对象指向分配的内存地址(此时 sInstance 就不是 null了);
由于 Java 编译器允许处理器乱起执行,以及 JDK1.5之前 JMM(Java Memory Model,即 Java 内存模型)中 Cache、寄存器到主内存会写顺序的规定,(2)和(3)的顺序是无法保证的,执行顺序可能是 1- 2 - 3也可能是 1 - 3 - 2,如果是后者,假设线程A在(3)执行完毕,(2)未执行时,此时被切换到线程B上,这时 sInstance 因为线程A已经执行过了(3),sInstance 已经是非空了,线程B直接获取 sInstance,再使用时就会出错,这种错误难以跟踪及发现,所以在 JDK1.5版本之后,调整了 JVM 具体化了 volatile 关键字,因此将 sInstance 的定义改成了
private static volatile Singleton mInstance = null;
就可以保证 sInstance 对象每次都是从主内存中读取。
优点:资源利用率高,第一次执行 getInstance() 时单例对象才会被实例化,效率高;
缺点:第一次加载时反应稍慢,也由于 Java 内存模型的原因偶尔会失败。
DCL 模式是使用最多的单例实现方式。
4 - 静态内部类模式
public class Singleton { private Singleton() {} public static Singleton getInstance() { return SingletonHolder.sInstance; } private static class SingletonHolder { private static final Singleton sInstance = new Singleton(); } }
当第一次加载 Singleton 类时并不会初始化 sInstance ,只有在第一次调用 getInstance() 方法时才会初始化 sInstance 。
优点: 确保线程安全并保证单例对象的唯一性。
推荐使用此单例模式实现方式。
5 - 枚举单例模式
public enum Singleton { INSTANCE; public void log() { //.... } }
优点: 写法简单,线程安全,并且在任何情况下它都是一个单列。
为什么说枚举单例是在任何情况下都是一个单列呢?那就是反序列化。
1至4的单例模式中,在反序列化情况下会出现重新创建对象的情况,反序列化时依然可以通过特殊的途径去创建一个类的实例,相当于调用该类的构造函数。上述实例中如要杜绝单例对象被反序列化时重新生成对象,必须加入如下的方法:
private Object readResolve() throws ObjectStreamException { return sInstance; }
在 readResolve() 方法中将 sInstance 对象返回,而不是默认的重新生成一个新的对象,对应枚举却不存在这个问题,因此即使反序列化也不会重新生成一个新的实例。
总结 无论哪种方式实现的单例模式,核心原理都是将构造函数私有化,并且通过静态方法获取一个唯一的实例,获取过程中必须保证线程安全、防止反序列化导致重新生成实例对象的问题。
以上总结内容参考 Android源码设计模型解析与实战 一书,书中详细阐述了各种设计模式及在 Android 源码中的使用,对设计模式想深入了解的同学,此书值得一看。