一、前言
单例模式是应用最广的设计模式之一,在使用这个模式时,单例对象的类必须保证只有一个实例存在,许多时候整个系统只需要拥有一个全局对象,这样有利于我们协调系统整体的行为。
二、定义
确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。
三、使用场景
确保某个类有且只有一个对象的场景,避免产生多个对象消耗过多的资源,或者某种类型的对象有且只有一个。
四、UML类图
屏幕快照 2019-04-23 上午10.43.29.png五、使用
单例模式有很多写法,下面就一一来看。
1、懒汉式
懒汉式,一开始不会创建该类的实例,而是等到真正用的时候才会去创建实例,即延迟加载。 屏幕快照 2019-04-19 上午11.26.13.png但是这种写法是线程不安全的。假设现在有两个线程:线程A和线程B,当线程A执行到上面代码中19行的时候,但是还没有给mInstance赋值,线程B执行到了18行,因为mInstance还没有复制,所以这个判断条件是true,线程B也可以走到19行,因此mInstance这个对象就被创建了两次,同时会返回最后执行的mInstance。下面我们来验证一下这个说法。
除了本身的主线程之外,我们再创建两个线程,先写一个线程类T, 屏幕快照 2019-04-19 上午11.56.34.png 然后在MainActivity中创建两个线程, 屏幕快照 2019-04-19 下午1.53.39.png 直接运行可以看到, 屏幕快照 2019-04-19 下午12.03.27.png这两个线程拿到的对象是一个,这是直接run的情况下,接下来我们看一下debug干扰线程的情况。
这里先介绍一下线程debug的使用,首先打上断点,然后在断点上右键,弹出一个弹出,选中Thread。 屏幕快照 2019-04-19 下午1.48.15.png 设置好之后,我们在T类的第8行代码打上断点,LazySingleton类的18行处打上断点,MainActivity类的23行处打上断点,然后开始debug app,
屏幕快照 2019-04-19 下午1.55.27.png
这个时候断点式主线程的,然后看左下角的Frames, 屏幕快照 2019-04-19 下午1.57.37.png
这个时候可以看到有main、Thread-2、Thread-3这三个线程,并且都是RUNNING状态,然后切到Thread-2线程上,然后看到Thread-2开始调用getInstance, 屏幕快照 2019-04-19 下午2.00.55.png
然后我们单步来到了18行, 屏幕快照 2019-04-19 下午2.01.27.png 这个时候通过debug方式调整多线程运行节奏,来触发现在这种写法在多线程的问题,接着单步,Thread-2来到19行,mInstance为null,赋值过程还没完成。 屏幕快照 2019-04-19 下午2.07.01.png 然后我们把线程切到Thread-3上,单步执行,来到18行 屏幕快照 2019-04-19 下午2.09.56.png 这个时候注意看Thread-3中mInstance为null,因为Thread-2没有赋值还没有完成,Thread-3继续单步,来到19行,这个时候切换回Thread-2,单步执行, 屏幕快照 2019-04-19 下午2.16.28.png 这个时候mInstance已经赋值上了,是5219,再切到Thread-3, 屏幕快照 2019-04-19 下午2.17.57.png 发现mInstance已经有值了,并且是5219,接着单步 屏幕快照 2019-04-19 下午2.19.08.png 这个时候mInstance变成了5220,也就是说这种懒汉式的单例写法在多线程下生成了不止一个实例,这仅仅是两个线程,如果是多个线程情况下,有可能会生成更多的实例,所以可能在初始化单例的时候会创建很多的对象,如果这个单例类的对象特别消耗资源,那很有可能造成系统故障,这是很危险的。这个时候我们再切回到Thread-2上,发现mInstance已经被Thread-3的值重新赋值了 屏幕快照 2019-04-19 下午2.24.36.png 这种情况下最后打印出来的lazySingleton还是一个对象。
我们换一种debug干扰方式,打印出来的lazySingleton就不是同一个对象了。还是上面的方式,只是在Thread-2走到19行的时候,切换到Thread-3,然后直接单步执行完输出结果,然后再去单步执行完Thread-3输出结果,可以发现最后输出的两个对象是不一样的。这个过程就不展示执行过程了,大家可以自己动手看看。所以我们不能被表面所迷惑。
接下来看看懒汉式的改进方案,
屏幕快照 2019-04-19 下午2.46.06.png 总结:通过同步锁我们解决了懒汉式单例的在多线程的一些问题,但是我们都知道同步锁会消耗资源,这里会有加锁和解锁的一些开销,会影响性能。
2、DoubleCheck双重检查
双重检查模式的写法: 屏幕快照 2019-04-19 下午4.47.09.png这种写法也是存在隐患的,其中隐患出现在18行和24行,18行,虽然判断了它是否为null,但是有可能它不为null,对象却没有初始化,也就是24行代码还没有执行完成。看一下24行,这一行代码经历了3个步骤,1、分配内存给这个对象,2、初始化对象,3、设置mInstance指向刚分配的内存地址。其中步骤2和3可能会重排序。
看一下多线程情况, 屏幕快照 2019-04-19 下午5.08.12.png
首先从上到下还是时间,左侧是线程0,右侧是线程1,其中线程1访问的对象并没有初始化完成,所以这个时候就有问题了,系统就会报异常了。那现在知道了问题所在,我们怎么解决呢?我们可以不允许线程0中2和3重排序,或者允许线程0中2和3重排序,但是不允许线程1看到这个重排序。
3、静态内部类
静态内部类模式代码:
屏幕快照 2019-04-19 下午6.56.16.png
我们来分析一下原理:先看一张图 屏幕快照 2019-04-19 下午6.01.06.png JVM在类的初始化阶段也就是class被加载后,并且被线程使用前都是类的初始化阶段,在这个阶段会执行类的初始化,在执行类的初始化期间,JVM会获取一个锁,这个锁会同步多个线程对一个类的初始化,基于这个特性,我们可以实现基于静态内部类的并且是线程安全的延迟初始化方案。那么看一下这个图还是线程0和线程1,在这种实现模式中,右侧的2和3的重排序对于前面讲的线程1并不会被看到,也就是非构造线程是不允许看到这个重排序的,因为之前讲的是由线程0来构造这个单例对象,初始化一个类,包括执行这个类的静态初始化,还有初始化在这个类中声明的成员变量,根据java语言规范,分为5种情况,首次发生的时候一个类即将立刻被初始化,这里说的类是泛指包括接口interface,假设这个类是A,现在说一下这几种情况都会导致A类被立刻初始化,首先第一种情况,有一个A类型的实例被创建,第二种A类中声明的一个静态方法被调用,第三种是A类中声明的一个静态成员被赋值,第四种情况,A类中声明的一个静态成员被使用,并且这个成员不是一个常量成员,这四种工作中使用的比较多,第五种,如果A类是一个顶级类,并且在这个类中有嵌套的断言语句,A类也会被立刻初始化。
看一下这个图,当线程0和线程1试图获取这个锁的时候,也就是获得Class对象的初始化锁,这个时候肯定只能一个线程获得这个锁,假设线程0获得了这个锁,线程0 执行静态内部类的 一个初始化,对于静态内部类,即使步骤2和3之间存在重排序,线程1也是无法看到这个重排序的,因为这个里面有一个Class对象的初始化锁。
回到代码,静态内部类这种核心方式在于InnerClass这个对象的初始化锁,看哪个线程先拿到,该线程就去初始化。
4、饿汉式
屏幕快照 2019-04-19 下午7.07.28.png5、序列化破坏单例模式原理解析及解决方案
用HungrySingleton作为实例,首先让HungrySingleton实现Serializable接口,然后修改MainActivity的代码, 屏幕快照 2019-04-22 上午10.38.31.png 看一下打印结果: 屏幕快照 2019-04-22 上午10.39.04.png 发现这两个对象不相等,这就违背了单例模式的初衷,通过序列化和反序列化拿到了不同的对象,我们只希望拿到同一个对象。那么怎么解决这个问题呢?很简单,只需要在HungrySingleton类中添加一个readResolve()方法即可。 屏幕快照 2019-04-22 上午10.55.21.png 再看一下打印结果: 屏幕快照 2019-04-22 上午10.56.16.png发现两个对象果然是相等的,这个问题就解决了。接下来我们看看为什么加一个这个方法就解决了这个问题,
6、反射攻击解决方案及原理分析
还是以饿汉式写法为例 屏幕快照 2019-04-22 下午2.28.09.png 看一下打印结果,发现两个对象不一样。 屏幕快照 2019-04-22 下午2.13.54.png 怎么防御反射呢?在HungrySingleton构造器中加入一段判空代码, 屏幕快照 2019-04-22 下午2.17.56.png 看一下这时的打印结果,抛出了异常, 屏幕快照 2019-04-22 下午2.19.48.png这种防御模式除了对饿汉式有用,还对静态内部类模式有效,因为他们都是在类加载的时候就会创建好对象,但是对其他写法没有效果,可以自己去试一下。那其他模式怎么防御呢?
7、Enum枚举单例
这种模式既可以防御反射也可以防御序列化,看一下代码,
屏幕快照 2019-04-22 下午3.02.53.png
我们主要关注枚举模式的序列化机制和反射攻击,枚举类天然可序列化机制能够强有力保证不会出现多次实例化的情况,即使是在复杂的序列化情况下反射攻击下,枚举类型的单例模式都没有问题。
六、单例模式在Android中的应用
LayoutInflater、WindowManager、ActivityManager、PowerManager。
七、总结:
单例模式的写法多种多样,每一种写法都有自己本身的优点与缺点,主要结合自己的需求去合理地使用。
网友评论