前言
设计模式系列(参考资料:《Android源码设计模式解析与实战》——何红辉、关爱明)
单例模式
Builder模式
原型模式
介绍
单例可以说是使用最为广泛,也比较简单的一种设计模式了。它的使用场景主要在:当我们在整个应用当中只需要一个实例,而且在不同地方获取都返回同一个实例的时候,就可以使用单例模式去处理。例如说封装一个SharedPreference
工具类,可以避免多次调用edit
新建EditorImpl
对象;例如数据库连接池等
定义
确保某一个类在系统运行过程中只有一个实例,而且自行实例化
并向系统提供这个实例
UML类图
Client
代表高层模式,也就是我们的客户端,它属于调用方;SingleTon
属于被调用方,其中的+
代表public
方法,-
代表private
方法
分类
单例模式主要分为四种实现方式,分别是:饿汉式、懒汉式 + 双重检查锁 + volatile
关键字、枚举类、静态内部类。不论是哪一种实现方式,核心原理都有以下几点:
- 1、
构造函数私有化
- 2、通过
静态方法
获取唯一
实例 - 3、获取过程中必须保证
线程安全
和尽量保证序列化安全
下面我们一个一个地分析实现方式。
1、饿汉式
public class SingleTon1 {
//1、声明的时候同时也初始化
private static SingleTon1 sInstance = new SingleTon1();
//2、构造函数私有化
private SingleTon1(){
}
//3、静态方法提供单例
public static SingleTon1 getInstance() {
return sInstance;
}
}
1.1 原理分析
1.1.1、线程安全原理
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException{
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
}
if (c == null) {
c = findClass(name);
}
}
return c;
}
这是类加载机制中的双亲委派机制
保证的,在抽象类ClassLoader
的loadClass
方法中,在加载类之前,先通过Class<?> c = findLoadedClass(name);
检查类是否已被加载,如果没有再通过parent.loadClass(name, false)
去不断调用父类的加载方法去加载类。
1.2 优缺点
饿汉式实现单例主要有以下优缺点:
优点:
- 1、线程安全:由类加载机制保证
- 2、第一次获取实例的效率会比懒汉式更高
- 3、写法简单
缺点:
- 1、不是懒加载方式,所以如果这个单例用不上的话就会导致浪费内存空间
- 2、会被反射所破坏
- 3、会被序列化所破坏
2、懒汉式 + 双重检查锁 + volatile
关键字
public class SingleTon2 {
private static volatile SingleTon2 sInstance = null;
private SingleTon2(){
}
public static SingleTon2 getInstance() {
if (sInstance == null){
synchronized (SingleTon2.class){
if (sInstance == null){
sInstance = new SingleTon2();
}
}
}
return sInstance;
}
}
2.1 原理分析
懒汉式因为是线程不安全的,并不推荐使用。所以通常我们使用懒汉式的时候都会配合volatile
和双重检查锁(DCL
),这里主要讲一下几个关键代码的作用:
2.1.1、volatile
关键字
这个关键字的作用是确保代码的可见性、有序性,但是不能保证代码的原子性
。
可见性
:多线程修改变量的时候都会将变量从主存读取到线程工作内存,如果线程A
修改了被volatile
修饰的变量,那这时候volatile
是会让其他B、C、D线程
中的变量失效的,强制要求B、C、D线程
重新去主存取新的值;
有序性
:CPU
为了运行效率,有了指令重排序这个机制。以新建对象为例子,A a = new A()
主要有以下几步:
- 1、在常量池看能不能找得到类元信息,如果没有,就通过双亲委派机制去加载类
- 2、给对象分配内存空间,如果内存是规整的话使用指针碰撞,不规整则使用空闲列表,这取决于垃圾回收算法
- 3、根据是否开启
useTLAB
来决定是否使用TLAB
,TLAB
就是用来在多线程分配的情景下提高效率的一种优化方法,具体原理是JVM
在伊甸区为每一个线程开辟一块私有空间,然后线程分配空间的时候在对应的空间内分配,实际上还是分配在堆内存的伊甸区内,这样就不会产生线程安全问题;如果没有开启,就通过CAS
方式对内存执行写入操作 - 4、为对象属性赋初始值
- 5、为对象设置对象头
- 6、执行构造函数进行初始化
- 7、将对象的内存引用地址赋值给
a
因为指令重排序的存在,所以7
并不一定是最后执行,也有可能会比6
更先执行,如果这种情况一发生,就会出现:
A
线程在执行sInstance = new SingleTon2();
这行代码的时候,B
线程在执行第一重检查锁if (sInstance == null)
,由于A
线程在执行过程中出现重排序,导致sInstance
并不为null
,所以B
直接return 一个还没初始化完全的sIntance
。
2.1.2、第一重检查锁if (sInstance == null)
这里主要是防止初始化完成后在getInstance
时synchronized
还被调用,从而影响性能
2.1.3、第二重检查锁if (sInstance == null)
这里主要防止一种情况:两个线程同时经过第一重检查锁,进入同步代码块,A
线程竞争到锁,B
进入等待池,在A
执行完之后其实sInstance
已经初始完毕了,但是如果不加这第二重判断,那B
会对sInstance
进行第二次实例化
2.2 优缺点
懒汉式 + 双重检查锁 + volatile
关键字 实现单例主要有以下优缺点:
优点:
- 1、线程安全:由双重检查锁和
volatile
关键字保证 - 2、懒加载实例
缺点:
- 1、如果
synchronized
一瞬间被多个线程所调用,直接膨胀成重量级锁的话或许会对性能有少许影响 - 2、会被反射所破坏
- 3、会被序列化所破坏
3、枚举类
public enum SingleTon4 {
INSTANCE;
public void doSomeThing(){
}
}
这是我最喜欢使用的一种方式,它也是《Effective Java》所力荐的一种单例实现方式。
3.1 原理分析
3.1.1 线程安全原理
它的实现原理是在反编译后会生成这样一个文件:
public final class SingleTon4 extends Enum<SingleTon4> {
public static final SingleTon4 INSTANCE;
...
}
可以看到INSTANCE
是static
的,也就是跟我们前面提到的饿汉式一样
3.1.2 序列化安全原理
枚举常量和其他对象序列化不同,序列化的枚举类型,只包含name
,而filed
值不在序列化文件里。在序列化过程中仅仅是将枚举对象的name
属性输出到结果中,反序列化过程则是通过valueOf()
方法根据name
查找枚举对象。因此枚举在序列化和反序列化过程中是保持同一对象的
3.1.3 反射安全原理
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
反射在通过newInstance
创建对象时,会检查该类是否枚举,如果是的话会抛出Cannot reflectively create enum objects
错误,导致无法被反射
3.2 优缺点
枚举 实现单例主要有以下优缺点:
优点:
- 1、线程安全:由类加载机制保证
- 2、反射安全
- 3、序列化安全
- 4、实现超级简单!
缺点:
- 1、无法懒加载
4、静态内部类
public class SingleTon3 {
private SingleTon3(){
}
private static class Inner{
private static SingleTon3 sInstance = new SingleTon3();
}
public static SingleTon3 getInstance(){
return Inner.sInstance;
}
}
4.1 原理分析
4.1.1 线程安全原理
线程安全的原因主要还是因为对于内部类Inner
来说,静态变量sIntance
只会被JVM
加载一次
4.1.2 懒加载原理
主要利用了外部类的加载并不会导致内部类被加载
这一特性
2.2 优缺点
静态内部类 实现单例主要有以下优缺点:
优点:
- 1、线程安全:由类加载机制保证
- 2、懒加载实例
缺点:
- 1、会被反射所破坏
- 2、会被序列化所破坏
总结
本文偏记录向,如果有不对的地方,还希望各种指出来呀,就这样~共勉
ps:换了一种行文风格,不再使用>
去包裹文字,希望看起来结构会更舒服一些。