设计模式之单例模式
定义:确保一个类只有一个实例,并提供对该实例的全局访问,其构造函数私有化。
单例模式的七种写法
1、饿汉模式
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton(){
}
public static Singleton getInstance(){
return instance;
}
}
这种方式在类加载时就完成了初始化,所以类加载较慢,但获取对象的速度快。这种方式基于类加载机制,避免了线程的同步问题。在类加载的时候就完成实例化,没有达到懒加载的效果。如果从始至终未使用过这个实例,则会造成内存的浪费。
2、懒汉模式
public class Singleton {
private static Singleton instance;
private Singleton(){
}
public static Singleton getInstance(){
if (instance == null){
instance = new Singleton();
}
return instance;
}
}
这种懒汉模式声明了一个静态对象,在用户第一次调用时初始化。这虽然节约了资源,但第一次加载时需要实例化,反应稍慢一些,主要是在多线程时不能正常工作。
3、懒汉模式(线程安全)
public class Singleton {
private static Singleton instance;
private Singleton(){
}
public static synchronized Singleton getInstance(){
if (instance == null){
instance = new Singleton();
}
return instance;
}
}
这种懒汉模式因为加了同步锁,所以是线程安全的,但是每次调用getInstance方法时都需要进行同步,这会造成不必要的同步开销,而且大部分时候我们是用不到同步的,所以不建议用这种模式。
4、双重检查模式(DCL)
public class Singleton {
private static volatile Singleton instance;
private Singleton(){
}
public static Singleton getInstance(){
if (instance == null){
synchronized (Singleton.class){
if (instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
这种写法在getInstance方法中队Singleton进行了两次判空:第一次是为了不必要的同步,第二次是在Singleton等于null的情况下才创建实例。在这里使用volatile,也会或多或少地影响性能,第一次加载时反应稍慢,在高并发环境下也有一定的缺陷。DCL虽然在一定程度上解决了资源的消耗和多余的同步、线程安全等问题,但其在某些情况下还是会出现失效的问题。
volatile作用:
(1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
(2)禁止指令重排序优化。(什么是指令重排序:是指CPU采用了允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理)。
DCL失效:
当一个A线程执行到instance = new Singleton()语句时,这里看起来是一句代码,实际上这不是一个原子操作,这句代码最终会被编译成多条汇编指令,它大致做了3件事:
(1)给Singleton的实例分配内存;
(2)调用Singleton()的构造函数,初始化成员字段;
(3)将instance对象指向分配的内存空间(此时instance就不是null了)
但是,由于Java编译器允许处理器乱序执行,以及JDK1.5之前JMM(Java Memory Model,即java内存模型)中Cache、寄存器到主内存回写顺序的规定,上面的第二和第三的顺序是无法保证的。也就是说,执行顺序可能是1-2-3,也可能是1-3-2。如果是后者,并且在3执行完毕、2未执行之前,被切换到线程B上,这时候instance因为已经在线程A内执行过了第三点,instance已经是非空了,所以,线程B直接取走instance,再使用时就会出错,这就是DCL失效问题。
5、静态内部类单例模式
public class Singleton {
private Singleton() {
}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
}
第一次加载Singleton是并不会初始化INSTANCE,只有第一次调用getInstance方法是虚拟机加载SingletonHolder并初始化INSTANCE。这样不仅能确保线程安全,也能保证Singleton类的唯一性,所以是非常推荐的一种单例写法。
6、枚举单例模式
public enum Singleton {
INSTANCE;
public void doSomeThing() {
}
}
默认枚举实例的创建时线程安全的,并且在任何情况下都是单例。前面讲过的机制单例模式,有一种情况会重写创建对象,那就是反序列化:将一个单例实例对象写到磁盘再读取回来,从而获得了一个实例。反序列化操作提供了readResolve方法,这个方法可以让开发者控制对象的反序列化。所以,如果要杜绝单例对象反序列化时重写生成对象,必须加入如下方法:
private Object readResolve() throws ObjectStreamException {
return SingletonHolder.INSTANCE;
}
枚举单例的优点就是简单,不过Android使用enum之后的dex大小增加很多,运行时还会产生额外的内存占用,其可读性也不高,因此官方强烈建议不要在Android程序里面使用到enum,枚举单例缺点也很明显。
7、容器实现单例模式
/**
* 单例容器类
*/
public class SingletonManager {
private SingletonManager() {
}
private static Map<String, Object> instanceMap = new HashMap<>();
public static void registerInstance(String key, Object instance) {
if (!instanceMap.containsKey(key)) {
instanceMap.put(key, instance);
}
}
public static Object getInstance(String key) {
return instanceMap.get(key);
}
}
/**
* 单例模板
*/
public class SingletonPattern {
SingletonPattern() {
}
public void doSomething() {
Log.d("==", "doSomeing");
}
}
// 具体用法
SingletonManager.registerInstance("SingletonPattern", new SingletonPattern());
SingletonPattern singletonPattern = (SingletonPattern) SingletonManager.getInstance("SingletonPattern");
singletonPattern.doSomething();
这种方式,根据key获取对象对应类型的对象,隐藏了具体实现,降低了耦合度。