这几天笔者刚开始看设计模式,这个东西虽然是一个比较抽象的东西,但是它更注重的是一种框架设计的思想,不只是Android,很多领域都会涉及到这个,感觉很重要。而且在Android中有很多类都是通过各种模式去实现的,也是为了以后能够更好地看懂源码和框架吧。
然而前几天在刚开始看单例模式的是就卡壳了-。+,原因是在看的时候,发现有很多涉及到的知识点已经忘记了,或者当时就没有好好的学,结果现在学设计模式的时候很懵逼。。。为此本人写了几篇文章,是在设计模式中运用到的但可能已经忘记的知识点(反正对于本人来说已经是忘记了0.+),当然这些文章会不断的更新的。
本片中可能涉及到的知识点:
设计模式前篇——多线程同步synchronized、volatile详解
单例模式介绍
首先简单介绍一下什么是单例模式,为什么要用这种模式。
我来举个栗子:
我们有一个类A,我们如果想要使用,那就直接new一个A出来吧。
public static void main(String[] args) {
// TODO Auto-generated method stub
A a = new A();
}
很简单,根本无难度,对吧。
假如说这个类A,是一个功能类的类(例如Android中布局加载器LayoutInflater),我们创建A出来只是为了实现一个功能,也就是说不管多少个A对象被创建出来,他们谁都能够完成这个功能,而且互不影响。你懂我意思吧。
这只是在main方法中创建了一个A对象然后使用,加入现在有1000个方法中需要通过A完成这个功能,如果说不是同时运行的还好,如果这1000个方法都同时创建一个A对象,要是所有的android程序都这么写,我估计手机内存扩展到10个G都不够用吧-。+。
上面这个案例,明确的阐述了单例模式的重要性,同时也说明了它的运用场景:
- 在程序中多处地方大量使用
- 被创建的多个对象都能够完成一功能,而且互不影响。
作用:为了节约内存开销,避免不必要的内存开辟。提高效率,提高资源利用率。
单例模式顾名思义,就是让这个类只产生一个对象。因为这种情况下需要这个对象的情况太多了,不如我就给你创建一个,这一个对象可以让全局都调用,反正一个和一百个都能完成任务,这样还节约了内存。
单例模式特点
一个类满足单例模式,主要有一下几个特点:
- 构造方法私有化
- 定义静态方法,且返回当前的对象
- 确保对象是唯一的!
- 确保在序列化和反序列化操作之后,保证还是一个对象
- 不允许被继承
前几点相信大家都能够看得懂,简单说一下就好:
因为只允许创建一个对象,那么我们在调用实例的时候肯定不会用到new方法去创建对象了。而是通过一个static方法,所以我们要保证在外部无法通过new方法去创建对象。而创建对象的过程,在类构造中或者在static方法中实现。
不允许被继承,这是一个标准的单例模式的规范,我们知道就好。
重要说一下第四点:关于序列化和反序列化笔者之前写过一篇文章,大家可以去看一下:Android中序列化与反序列化。也就是说,我们在将对象进行序列化写入、反序列化读取,通过字节序列重新恢复成的对象,一定要保证是一个对象。
单例模式UML图结构
我觉得单例模式可以说是这里面最简单的一个设计模式了,因为他的UML类图实在是太好画了。。。。
只是一个依赖关系,这里只是那MainActivity举例了,当然还有很多地方调用到SingleTon。
模式变种
对于单例模式,相信大家之前有看到过一些,也有用过,其实那只是单例模式的一种或两种。下面是几种常用到的单例模式的变种:
1.饿汉式
相信大家之前最常用过得就是这种单例模式了,我们来看一下他的构造:
/*
* 饿汉式
* */
public class SingleTon {
private static SingleTon instance = new SingleTon();
private SingleTon() {
System.out.println(Thread.currentThread().getName() + " 创建成功");
}
public static SingleTon getInstance() {
return instance;
}
}
优点:线程安全。因为这种情况下在类加载的时候就已经完成了初始化,我们不用去担心在多个线程调用时产生重复创建的问题(现在说可能不太明白,我们等会对比下面的情况一起说)
缺点:
1.可能会占用不必要的内存空间。
由于是在类加载的过程中new的对象(这里涉及到类的生命周期的一些知识,笔者之后会补一篇关于这个的文章),所以无论我们用不用,他都会创建,这就有一种情况了。确实我们通过这样实现了唯一的对象,但是我们在不用他的时候,他还是会在占用着内存,如果所有的单例都这样写,一个项目中有成百上千个单例,全部都占用着一块内存,我想这也是我们不愿意看到的。
2.类加载先后顺序与声明之前的问题。
现在我们只需要知道:饿汉式可能会因为声明顺序,而导致创建初始化数据出错。造成异常。之后本人会出一篇文章细说这个问题。
有没有这样一种情况呢:让他在需要的时候创建,不需要的时候先不要创建,以此来达到节约内存的效果。
2.懒汉式
/*
* 懒汉式
* */
public class SingleTon {
private static SingleTon instance;
private SingleTon() {
System.out.println(Thread.currentThread().getName() + " 创建成功");
}
public static SingleTon getInstance() {
if(instance == null) {
instance = new SingleTon();
}else {
System.out.println("已经有实例");
}
return instance;
}
}
与饿汉式相比,懒汉式只是在其基础上做了一点小小的变动:在声明时不创建对象,而是在获取的方法中判断是否已经创建,如果没有创建就new对象并返回。
优点:这种情况相比于饿汉式来说,他的性能更高一些,因为他避免了在没有使用的时候占用了不必要的内存。
缺点:
问题1:线程安全问题。
我们喜欢拿这两种变种进行对比,因为他们是在是太像了。而且优缺点貌似还是正好反过来的。在懒汉式情况下,我们需要考虑线程安全的情况:在多个线程并发进行的时候,如果都要用到SingleTon对象的时候,那他们都在调用getInstance方法,这样会导致什么结果出现呢?
public static void main(String[] args) {
// TODO Auto-generated method stub
for(int i = 0;i < 10;i ++) {
new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
SingleTon.getInstance();
}
}).start();
}
}
我们在main方法中开启了10个线程,里面都获取SingleTon实例,看一下结果会发生什么:
Thread-0 创建成功
Thread-1 创建成功
Thread-2 创建成功
已经有实例
已经有实例
已经有实例
已经有实例
已经有实例
已经有实例
已经有实例
发现有三个线程同时创建了对象,这是因为线程是并发进行的,他们同时调用了getInstance方法,在创建还没有完成的时候,另一个线程获取到的信息是instance == null,所以他也会创建,这就导致了多个实例被创建,这是我们不希望看到的。
那我们想一个办法——添加synchronized同步锁吧。(关于同步锁以及下面重排序的相关知识:设计模式前篇——多线程同步synchronized、volatile详解)
/*
* 懒汉式
* */
public class SingleTon {
private static SingleTon instance;
private SingleTon() {
System.out.println(Thread.currentThread().getName() + " 创建成功");
}
public static synchronized SingleTon getInstance() {
if(instance == null) {
instance = new SingleTon();
}else {
System.out.println("已经有实例");
}
return instance;
}
}
这种情况下在同一时刻只能有一个线程使用getInstance方法,这样也就不存在同时创建的问题了。
但是它真的是完美的吗?
问题2:添加同步锁后,出现阻塞,耗费性能问题。
由于我们添加了同步锁,所以同一时刻只有一个线程可以使用getInstance方法,其他线程在没有获取到同步锁的时候会进行阻塞,就是在那里等着。
如果这一个同步的方法需要创建1分钟,那和100个线程就在那里傻等着,这种情况真的要死了。。。。。
所以这样也不是很完美。接下来我们看一下第三种变种。
3.双重检查锁
先看一下这种方式的半成品代码:
/*
* 双重检查锁(半成品)
* */
public class SingleTon {
private static SingleTon instance = null;
private SingleTon() {
System.out.println(Thread.currentThread().getName() + " 创建成功");
}
public static SingleTon getInstance() {
if (instance == null) {
synchronized (SingleTon.class) {
if (instance == null) {
instance = new SingleTon();
}
}
}
return instance;
}
}
这里面主要修改的还是getInstance方法,他相比与饿汉式跟懒汉式,都要优越一些,因为他即保证了线程安全,实现了单例,又一定程度上解决了性能耗费的问题。也实现了延时初始化。
这里主要说一下为什么相比于懒汉式,它的性能更优越呢:在懒汉式中我们是给getInstance方法整个加锁,这就导致了可能会出现多个线程同时阻塞状态,造成了性能耗费,而在双重检查中,我们用同步代码块代替了同步静态方法:
- 给同步代码块外面留了一个第一个if判断:是否已经创建了SingleTon对象,如果他已经创建了,那么其他线程就不用在这个方法中傻等着了,直接return获取。这样就避免了在创建过程中全部线程阻塞的问题。
- 再看一下同步代码块中第二个if判断:如果为空就创建对象。由于添加了synchronized同步锁,所以这段代码具有原子性,会在执行完毕,在主存中同步数据后,释放锁。
这个东西理解起来比较抽象,大家还是要自己推敲一番啊。。。
简单概括一下:懒汉式是在多个线程中等待获取同步锁,而双重检查给这些线程留了一道门(外层if),给了他们一个不用频繁获、取释放同步锁的机会。这样一对比性能是优越了不少吧。
但这种方法也不是十分完美的,他很可能会出现一个问题——双重检查锁失效。
原因1:重排序问题
我们跳回到这部分代码:
public static SingleTon getInstance() {
if (instance == null) {
synchronized (SingleTon.class) {
if (instance == null) {
instance = new SingleTon();
}
}
}
return instance;
}
其实主要造成这个问题的,是这一行代码:
instance = new SingleTon();
这是一个new创建对象的过程。我觉得我们又有必要补充一些基础知识了:
这个操作大概可以拆分为几个部分:
- 栈开辟空间给instance引用存放。
- 堆开辟空间给SingleTon对象做准备。
- 初始化对象(构造方法)
- 让instance引用指向堆中的内存空间。
关于重排序问题在同步锁的篇中大概说到了,大家可以翻到上面看一下链接。
简单来说,重排序是系统为执行效率更高搞出来的东西,它让我们的指令顺序变乱。
比如上面的1->2->3->4,他很有可能出现1->2->4->3,在执行到4的时候,我们的instance已经不为空了,但是还没有调用构造方法初始化,如果说这个时候其他线程在第一层if判断,会是不为null的结果,那么他就直接返回instance了,这样一来会导致我们的数据出现问题,最后可能会出现一颗老鼠屎,坏了一锅汤的情况。
而对于这种情况我们也知道, 只要给instance属性添加volatile关键字,就可以避免重排序的问题了。
所以最完美的双重检查锁代码如下:
/*
* 双重检查锁(完成品)
* */
public class SingleTon {
private static volatile SingleTon instance = null;
private SingleTon() {
System.out.println(Thread.currentThread().getName() + " 创建成功");
}
public static SingleTon getInstance() {
if (instance == null) {
synchronized (SingleTon.class) {
if (instance == null) {
instance = new SingleTon();
}
}
}
return instance;
}
}
但是注意,在j5之前是没有volatile关键字的,所以说在此之前还是会出现双重检查失效的问题。
4.静态内部类
静态内部类可以说相对于上面几种最简单而且最完美的一种情况了。而且是官方推荐的一种写法。
代码如下:
/*
* 静态内部类
* */
public class SingleTon {
public static SingleTon getInstance() {
return SingleTonHolder.instance;
}
static class SingleTonHolder{
private static SingleTon instance = new SingleTon();
}
}
这种写法看上去太舒服了,而且他既做到了延时初始化,节约不必要的内存开销(因为静态内部类的类加载过程是在调用时候执行,如果不使用就不会加载,也就不会创建instance对象了),又保证了线程的安全。
5.枚举
这个是一种比较新型的写法:
/*
* 枚举
* */
public enum SingleTon {
INSTANCE;
private String asd = "我只是一个普通的属性!!!";
public void asd() {
System.out.println("我只是一个普通的方法!!! "
+ asd);
}
}
我们什么都不用操作,只需要创建一个INSTANCE实例就好。因为编译过后回味INSTANCE生成一个对应的SingleTon对象,而且枚举类型经过编译后生成的class对象还实现了序列化,又保证了线程安全,岂不是美滋滋。(关于枚举的文章请见:设计模式前篇——枚举)
6.集合——对象管理
对于这种变种,已经不能算是标准的单利了,但我觉得还是有必要了解一下:
/*
* 集合
* */
public class SingleTonManager {
private static Map<String, SingleTon> map = new HashMap<>();
public static void putValue(String key,SingleTon value) {
SingleTon instance = map.get(key);
if(instance == null)
map.put(key, value);
}
public static SingleTon getVaule(String key) {
return map.get(key);
}
}
class SingleTon{
}
使用场景(推荐)
我们列举了这么多变种,发现前面几个标准的单利中,几乎每个变种都有好坏,难道就没有一个完美的模式吗?
其实这种想法多虑了,我们只需要针对不同的情况去应用不同种类的单例就好了,假如说你只是在一个线程中使用单例,你搞一个双重检查?没事找事。
在多线程下:推荐使用静态内部类的方式。
在单线程下:推荐使用懒汉式。
今天的文章就到这里了,如果有意见或者探讨的朋友欢迎在下方留言,喜欢的朋友点赞关注一波哦。
谢谢大家的支持!!