一.简介
单例模式是一个十分常见的设计模式,一个对象实例如果在创建的过程十分消耗资源,且整个 app 系统只需要拥有一个对象的时候,就可以使用单例模式,在单例模式下,一个类只有一个实例并且在实例化后可以向系统的各个地方提供这个实例,不再创建新的实例。
二.面对的问题
单例模式的实现关键就是确保这个类在整个系统运行过程中有且只有一个实例,因为在 Android 系统中存在者多线程,序列化等情况,在这些情况下很容易产生类多次实例化的情况,因此在实现单例模式的时候就需要特别注意这个问题。
实现单例模式的关键点
- 构造函数不对外开放,即为 private 而不是 public
- 通过一个静态方法或者枚举类型返回单例对象
- 确保在多线程的环境下确保有且只有一个对象
- 确保在反序列化的时候不会重新构建新的对象
三.实现方式
1.非懒加载模式 (线程安全)
1 | /** |
由于 instance 在类初始化过程已经被赋值且是 final 修饰,因此就不存在线程安全的问题,但是这种情况不管这个单例有没有使用都会对 instance 进行初始化。序列化问题则通过添加 readResolve 方法解决,通过在这个返回 instance 本身就可以保证在反序列化的时候是同一个实例。
2.懒汉模式 (粗暴地解决线程同步问题)
懒汉模式就是声明一个静态的变量,在获取的时候对方法进行同步,并且在第一次调用的时候进行初始化。
1 | /** |
这种方式每次调用的时候都进行同步,因此在不涉及线程安全的情况时,进行同步就多余了。而对于序列化的解决和第一种方式一样。
3.双检测锁模式 Double Check Lock ,DCL (不是很优雅的优化线程同步问题)
1 | /** |
首先看到代码中通过两次检测 instance 是否为 null,第一次 来避免不必要的同步,而第二次检测时为了尽量保证线程同步,之所以说是尽量使因为这种情况还不能绝对地保证线程同步。
理解上面这句话首先要明白在程序运行的时候, instance = new Singleton();这句代码会被编译器编译成多条指令,CPU 每次执行时只能执行一条指令,这些指令大致完成 3 件事;
- 1.给实例分配内存
- 2.调用构造函数,初始化成员字段
- 3.将 1 中内存空间的地址赋值给 instance,经过 3 后 instance 才不为 null
但是由于虚拟机的原因实际上上面的指令不一定是 1->2->3, 有可能是 1—>3->2.因此就产生了一个问题:
- 首先 线程 A 获得对象锁后,执行到 instance = new Singleton(),这时如果 3 先执行,还未执行 2 的时候,即 instance 已经不为 null, 但是还未初始化。
- 接着线程 B 进入方法,在进行第一个 null 判断(在同步代码块外面),instance 不为 null, 所有直接返回,但是实际上 instance 还未初始化,所以可能出错。
在 JDK1.5 引入了 volatile 后就可以解决这个,因为volatile 声明的变量禁止了指令的重新排序,所以就不存在上述的问题。
而对于序列化的解决和第一种方式一样。
4.静态内部类模式(线程安全)
1 | /** |
这种实现方式实际上就是对第一种方式的优化,解决了不能懒加载的问题。对于静态内部类,并不是在外部类加载的就进行初始化,而是在使用到的时候才进行类加载,因此将 instance 放在静态内部类中,只在第一次调用 getInstance 的时候才会进行初始化,解决了线程同步问题且实现了懒加载。
而对于序列化的解决和第一种方式一样。
5.单元素枚举单例(最佳)
因为枚举类的实例实际上都是线程安全的,且在序列化的过程中也都是同一个对象,不用像前面一样添加方法,因此可以说枚举单例是在任何情况下都只有一个对象。
1 | /** |