单例模式全解

时间:2021-03-31 12:05:59

公司最近在招聘新人,老大面试完回来偶尔会跟我吐槽一下,他经常会让面试者手写单例模式的代码。下图就是一个例子:
单例模式全解
我记得他当时回来跟我说今天面试这哥们不行啊,不知道是不是太久没写代码手生了,一个单例都写不了,然后说你来找找这段代码里面的问题,我也就吧啦吧啦的说了一通。讨论完这个问题,我看了一下这个面试者的简历,211院校计算机科班出身,四年Android开发经验,应聘职位Android架构师,再看回他写的这段代码,简直是赤裸裸的讽刺,毫无意外地,他倒在了一面。

经此一事,笔者除了诧异之外也在反思,虽然在日常开发中经常使用单例模式,也基本掌握基本的使用方法,但是总感觉很多时候是为了用单例模式而在用单例模式,还有一些高级用法也没有真正了解,所以赶紧去查阅资料,踏踏实实地由简至繁研究了一遍,谨以此文记录。本文会分两大部分内容,一是单例模式的使用场景,二是单例模式的实现方法。如果你现在看完上面的代码还没有找出代码中的所有问题,看完本文你一定可以做到。

一、单例模式的使用场景
单例模式用于确保某一个类有且只有一个实例,避免产生多个对象消耗过多的资源。所以当你所要定义的类会消耗较多的资源,例如访问IO和数据库等资源,就要考虑使用单例模式了。举个例子,在Android开发中出色的图片加载框架ImageLoader就使用了单例模式。因为ImageLoader中含有线程池、缓存系统、网络请求等等,比较耗费资源,如果没加载一张图片就去新建一个ImageLoader的实例则非常耗费资源,所以程序的设计者使用了单例模式,以让一个应用程序中有且仅有一个ImageLoader的实例。

二、单例模式的实现方法
实现单例模式主要有以下几个重点:
(1)构造方法不对外开放,一般为private
(2)通过静态方法返回实例
(3)确保类的实例有且仅有一个,特别是在多线程的环境下
(4)尽量确保线程安全
掌握内在思想后,就要开始实践了,单例模式的实现方法多种多样,且各有千秋,下面由简至繁一一举例,并且进行点评。
1、饿汉模式

public class Singleton {
private Singleton() {};
private static final Singleton single = new Singleton();
public static Singleton getInstance() {
return single;
}
}

上述这种实现方法叫饿汉模式,从代码中可以看到,外届只能通过Singleton.getInstance()方法获得Singleton的实例(single),而这个实例是静态对象,并且在声明的时候就初始化了,这就保证了Sigleton对象的唯一性,以后不再改变,所以天生是线程安全的。
2、懒汉模式

//懒汉式单例类.在第一次调用的时候实例化自己
public class Singleton {
private Singleton() {};
private static Singleton single=null;
public static synchronized Singleton getInstance() {
if (single == null) {
single = new Singleton();
}
return single;
}
}

懒汉模式同样将构造函数私有化,对外提供Singleton.getInstance()方法获得Singleton的实例(single),不同的是饿汉模式中在类加载的时候就已经初始化了single实例,而懒汉模式中只有开发者调用了Singleton.getInstance()才去创建Single的实例对象(single),同时,细心地读者应该发觉Singleton.getInstance()方法加了一个synchronized 的关键字,这个关键字就是用来保证线程安全的。这种方法实现了单例模式也保证了线程安全,但是每次调用Singleton.getInstance()方法都需要进行同步以保证线程安全造成了很多不必要的同步开销,消耗资源。这个就是懒汉模式的缺点。
3、双重加锁(Double Check Lock)

public class Singleton {
private Singleton() {};
private volatile static Singleton single=null;
public static Singleton getInstance() {
if (single == null) {
synchronized (Singleton.class) {
if (single== null) {
single = new Singleton();
}
}
}
return single;
}
}

Double Check Lock双重加锁,简称DCL,这种方式实现单例既能够在需要使用时才初始化单例,进行了同步保证了线程安全,也提高了资源的利用率。它的亮点在于Singleton.getInstance()方法中的对single进行了两层判空,第一层判空是为了避免不必要的同步,第二层判空是为了在single为null的情况下才创建实例。读者应该注意到单例对象single 定义添加了关键字volatile ,添加该字段是因为程序执行
single = new Singleton();
这个语句时其实做了三步工作,
(1)分别是给Singleton的实例分配内存,
(2)调用构造函数初始化成员字段,
(3)将single对象指向分配的内存空间(single不再为null),
由于Java虚拟机是乱序执行的,所以执行顺序可能是(1)(2)(3),也有可能是(1)(3)(2),在多线程情况下,假如(3)执行完成,(2)未执行,线程从A切换线程B,因为此时single不为null,所以线程B直接取走了未执行步骤(2)的single,因为此时single未调用构造函数初始化,所以B线程在使用single时会出错,此时双重加锁就会失效,上面添加的volatile关键字就是为了解决这个问题,它可以保证single每次都从主存中读取。但是volatile也会影响性能,一般来说在Android开发中,高并发的情况比较少见,所以在实际开发中大多数开发者选择把volatile 字段去掉。
4、静态内部类方式

public class Singleton {
private static class Holder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() {
return Holder.INSTANCE;
}
}

DCL虽然在一定程度上解决了资源消耗、多余的同步、线程安全等问题,但是在也会存在失效的可能,严重会导致程序崩溃。在《Java并发编程实践》中也谈到了这个问题,并直指这种优化是丑陋的不赞成使用,而提倡了上面这种静态内部类的实现方式。这种方式比上面1、2、3都好一些,既实现了线程安全,又避免了同步带来的性能影响,保证了单例对象的唯一性,同时延迟了单例的实例化。
仁者见仁智者见智,在Android开发实践中,通过阅读别人的代码发现,大部分开发者偏向于使用第三种双重加锁的实现方式,不过一般会把volatile 关键字去掉,个人建议还是加上避免出现失效的情况,同时第四种方法得到圣书《Java并发编程实践》的力推,优点也是显而易见,所以个人推荐第三第四种实现方式。看完本文,应该可以找出文首代码片段中的错漏了吧?

如果喜欢或对本文有疑问,烦请点赞或评论,文章一般会优先推送到Android小黑屋公众号,可扫码关注公号,您们的支持是我不竭的动力。

单例模式全解

                 扫码关注Android小黑屋