本文出自博客Vander丶CSDN博客,如需转载请标明出处,尊重原创谢谢
博客地址:http://blog.csdn.net/l540675759/article/details/69488764
前言
1.本篇博客属于笔记性质,主要整理归纳知识点,方便日后.
2.本篇的内容是基于”Android源码设计模式”.
3.如果有不足的地方,请大家指出,作者会及时更改.
单例模式的介绍
单例模式是应用最广的模式之一,在应用这个模式时,单例对象的类必须保证只有一个实例存在.
许多时候整个系统中只需要拥有一个全局对象,这样有利于我们协调系统整体的行为.
如,在一个应用中,应该只有一个ImageLoader实例,这个ImageLoader中又包含线程池,缓存系统,网络请求等,很消耗资源,因此在这种情况下需要使用单例模式.
单例模式的定义
确保某一个类只有一个实例,而且自行实例化并向整个系统提供整个实例,
单例模式的使用场景
确保某个类有且只有一个对象的场景,避免产生多个对象消耗过多的资源,或者某种类型的对象只应该只有一个.
例如:访问IO和数据库等资源.
单例模式UML类图
角色介绍:
Client —— 高层客户端(也可以理解成对单例类有需求的对象)
Singleton —— 单例类
单例模式的几种实现方式
懒汉式
public class Singleton {
private static Singleton instance;
//构造方法私有化
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
懒汉式特点:
1.懒汉单例模式的优点是单例只有在使用时才会被实例化,在一定程度上解决资源.
2.懒汉单例模式的缺点是第一次加载时需要及时进行实例化,反应稍慢,最大的问题是每次调用getInstance()都进行同步,造成不必要的开销.
这种模式一般情况下不建议使用.
Double CheckLock(DCL)实现单例
public class SingletonDCL {
//在JDK1.5之后 volatile直接使Java处理器在创建这个对象上,顺序执行,不可乱序执行.
private static volatile SingletonDCL instance;
private SingletonDCL() {
}
public static SingletonDCL getInstance() {
//第一道锁,主要是为了避免不必要的同步
if (instance == null) {
synchronized (SingletonDCL.class) {
//第二道锁为了在null的情况下,创建实例.
if (instance == null) {
instance = new SingletonDCL();
}
}
}
return instance;
}
}
双重锁检查单例的特点:
1.资源利用率高,第一次执行getInstance时单例对象才会被实例化,实现了懒加载.
2.第一次加载时反应稍慢,并且在JDK1.5之前,不使用volatile关键字,可能在并发环境中出现问题.
DCL模式是使用最多的单例实现方式,它能够在需要时才实例化单例对象,并且能够在绝大多数场景下保证单例对象的唯一性.
除非你的代码在并发场景比较复杂或者低于JDK1.6版本下使用,否则,这种方式一般能够满足需求.
静态内部类单例模式
public class SingletonN {
private SingletonN() {
}
/**
* 静态内部类
*/
private static class SingletonHolder {
//内部类静态初始化
private final static SingletonN sIntance = new SingletonN();
}
public static SingletonN getInstance() {
return SingletonHolder.sIntance;
}
}
DCL虽然在一定程序上解决了资源消耗、多余的同步、线程安全等问题,但是,它还是在某些情况下出现失效的问题。这个问题被称为双重检查锁定(DCL)失效,在《Java 并发编程实践》一书的最后谈到了这个问题,并指出这种优化还是存在问题的。
静态内部类单例模式的特点:
当第一次加载 Singleton 类时并不会初始化 sIntance,只有在第一次调用 Singleton 的 getInstance 方法时才会导致 sIntance 被初始化。
因为,第一次调用 getInstance 方法会导致虚拟机加载 SingletonHolder 类,这种方式不仅能够确保线程安全,也能够保证单例对象的唯一性,同时也延迟了单例的实例化,所以这是推荐使用的单例模式实现方法。
枚举单例
public enum SingletonEnum {
SINGLETON_ENUM;
public void doSomething(int i) {
//默认枚举实例创建就是线程安全的
}
}
枚举单例的特点:
首先,枚举单例的写法比较简单
枚举在 Java 中与普通的类是一样的,不仅能够有字段,还能够有自己的方法。最重要的是默认枚举实例的创建是线程安全的,并且在任何情况下它都是一个单例。
而且枚举反序列化也不会重新生成新的实例。
使用容器实现单例模式
public class SingletonManager {
//储存单例的容器
private static Map<String, Object> objMap = new HashMap<>();
private SingletonManager() {
}
/**
* 添加单例容器
*
* @param key 单例容器的 key
* @param instance 单例容器的 引用
*/
public static void registerService(String key, Object instance) {
if (!objMap.containsKey(key)) {
objMap.put(key, instance);
}
}
/**
* 获取单例
*
* @param key 单例容器的 key
* @return 容器中的单例对象
*/
public static Object getService(String key) {
return objMap.get(key);
}
}
使用容器实现单例的特点:
首先需要在程序的初始,将多种单例类型注入到一个统一的管理类。
然后使用的时候根据 key 获取对象对应类型的对象。这种方式使得我们可以管理多种类型的单例,并且在使用时可以通过统一的接口进行获取操作,降低了用户的使用成本,也对用户隐藏了具体实现,降低了耦合度。
个人来看,感觉这种方式更多的还是管理单例的工具类来使用,并且放入存储的对象不能太大,因为这些资源都是在程序开始的使用初始化。
单例模式的总结
不管哪种形式实现单例模式,它们的核心原理都是将构造函数私有化,并且通过静态方法获取一个唯一的实例,在这个获取的过程中必须保证线程安全、防止反序列化导致重新生成实例对象等问题。
而选择哪种实现方式取决于项目本身,如是否是复杂的并发环境、JDK版本是否过低、单例对象的资源消耗等。
DCL失效问题
在 JDK1.5之前,DCL实现单例一直是这样的:
public class SingletonDCL {
private static SingletonDCL instance;
private SingletonDCL() {
}
public static SingletonDCL getInstance() {
//第一道锁,主要是为了避免不必要的同步
if (instance == null) {
synchronized (SingletonDCL.class) {
//第二道锁为了在null的情况下,创建实例.
if (instance == null) {
instance = new SingletonDCL();
}
}
}
return instance;
}
}
DCL模式下的单例,亮点都在 getInstance 方法中对 instance 进行了两次判空:
第一层判断主要是为了避免不必要的同步,第二层的判断则是为了在 null 的情况下创建实例。
在不涉及到并发,多线程的情况下,上述 DCL的模式是一种很好的选择,然后在高并发,多线程的情况下,DCL单例就会出现问题。
sInstance = new SingletonDCL 分析
上述代码表述的是生成一个实例的代码,但是其不是一个原子操作。这句代码最终会被编译成多条汇编指令,它大致做了3件事情:
(1)给 SingletonDCL的实例分配内存
(2)调用 Singleton()的构造函数,初始化成员字段
(3)将 sIntance 对象指向分配的内存空间(此时 sInstance就不是 null 了)
但是,由于 Java 编译器允许处理器乱序执行,以及 JDK1.5之前 JVM中 Cache、寄存器到主内存回写顺序的规定,上面第二条和第三条是无法保证顺序的。
就是执行顺序可能出现两种结果:
(1) -> (2) -> (3) 正常情况
(1) -> (3) -> (2) DCL失效情况
如果是第二种顺序,并且在3执行完毕,2未执行之前被切换到 B 线程上,这时候 sIntance 因为在线程 A 内执行过了第三点,sInstance 已经非空了,所以,线程 B 直接取走 sInstance,再使用时就会出错,这就是 DCL失效问题。
而且这种错误,难以重现和难以追踪。
在 JDK1.5之后,SUN官方已经注意到这种问题,调整了 JVM、具体优化了 volatile 关键字,如果是 JDK1.5之后的版本,在定义 Instance 时加上 volatile 关键字(用来保证 JVM生成该变量的顺序),就不会发生 DCL失效的问题。