我们搞技术的,面试不免要问到设计模式,而其中出场率最高的就是今天要说的单例模式了。
在这儿分享一个我面试的故事吧。
当时是去一家较小的互联网公司,工资开的挺高,我想要求自然也不会很低。进去之后,面试官和我印象中的消瘦格子衫程序员完全不一样,高大偏胖,眼神犀利,有种不怒自威的感觉,不免有些紧张。
前边都是问一些项目经历,一些基础知识,答得还算可以,心态也慢慢的平静下来。到最后问到设计模式,我放下的心又立即悬了起来,我想糟了,除了单例,其它的虽然看过一些书,但是没有怎么实际用过,当然也说不出什么,在大神面前不是班门弄斧吗?就告诉他,我实际项目中只用过单例。他就让我手写一下单例模式,我心中大喜,单例在网上看过,默写没有什么问题。接过纸,刷刷几笔,就将双重检查锁定单例(文章下边会有讲)写了出来,因为百度单例模式,弹出的博客最终几乎都会将这种写法作为最终的也是最好的写法。我很自信,眼神中流露出几丝得意。他看了一下,眉头紧皱,我有点儿不解,也不敢贸然去问为什么,只好默不作声看着他。大概过了五六秒,他指着我的代码,问我,还有更好的写法吗?我不解,心想,这不就是最好的写法吗。我以为他没有看懂,忙指着代码给他解释。他不耐烦的打断了我,继续追问,你平时写单例就是这么写的吗?我心里直犯嘀咕,默默的点点头。但我到此时还是坚信,我写的没有问题,咽了口唾沫,尝试轻声问道,那应该怎么写?他显然已经不想回答了,说我送你下去吧。我收拾好简历,垂头丧气的跟着他下楼了,但我依旧不死心,问他,我到底差在哪儿?应该怎么去提高?他转过头,愣了一下,然后笑了一下,说,基础还可以,但是项目经验太少了,公司要求比较高,你不要拘泥于一些网上的demo,去 github 上找一些完整的项目,仔细研究一下。然后他就转身离开了。
后来仔细想想,他说的很对,但是这个单例模式的问题一直困扰了我好久。
因为我是做 Android 系统二次开发,有一天,突然想分析一下源码中使用的设计模式,凑巧,这个谜团就解开了,于是就有了这篇博客。
确保某一个类只有一个实例,并且自行实例化并向整个系统提供这个实例。在有线程池,网络操作,缓存操作等很耗费资源的对象,我们不希望构造多个这样的实例,所以需要单例模式。
//饿汉单例模式
public class Singleton {
private static final Singleton singleton = new Singleton();
// 私有构造函数
private Singleton(){}
// 公有的静态函数,对外暴露获取单例对象
public Singleton getInstance(){
return singleton;
}
}
//懒汉模式
public class Singleton{
private static Singleton singleton;
private Singleton(){}
public static synchronized Singleton getInstance(){
// 只有在第一次调用该方法的时候才会初始化
// 并且加了 synchronized 关键字(线程安全),但是每次调用
// 都会线程同步,这样会消耗很多不必要的资源,不建议使用
if(singleton == null){
singleton = new Singleton();
}
return singleton;
}
}
// 双重检查锁定单例(Double Check Lock)
public class Singleton{
private static Singleton singleton;
private Singleton(){}
public void doSomething(){
System.out.println("do something");
}
public static Singleton getInstance(){
if(singleton == null){
synchronized(Singleton.class){
if(singleton == null){
singleton = new Singleton();
}
}
}
return singleton;
}
}
看上去似乎没有问题,双重检查锁定单例解决了懒汉模式所出现的每次都同步的问题,但是在并发或者是连续多次获取并调用 doSomething() 方法时会出问题,这就是 DCL 失效问题。
这里边就涉及到了编译原理的一些知识了。我们的代码最终会编译成一条条汇编指令来执行,拿
singleton = new Singleton();这句来说,会被编译成三条汇编指令
而 java 编译器允许处理器乱序执行,所以上述三条汇编指令的执行顺序可能并不是我们理解的 1-2-3 这么简单,也有可能是 1-3-2。如果是后者,就会出错了。看图:
谷歌也意识到了这个问题,调整了 jvm ,在 jdk 1.5 之后,具体化了 volatile 关键字,现在我们只要在获取单例的方法前加上 volatile 关键字就可以了。
public volatile static Singleton getInstance(){
// 但是 volatile 也是一个很耗费资源的操作
......
}
既然 volatile 是一个很耗费资源的操作,那有没有更好的方法呢?
// 静态内部类单例模式
public Singleton{
private Singleton(){}
public static Singleton(){
return SingletonHolder.singleton;
}
private static class SingletonHolder{
private static final Singleton singleton = new Singleton();
}
}
这种写法在第一次调用的时候才会实例化,并且也保证了单例对象的唯一性,所以建议使用这种方法来书写单例模式。
当然,还有其他的书写方法
// 枚举单例
class Singleton{}
public enum SomeThing{
Instance;
private Singleton singleton;
// 在枚举中我们规定构造方法为私有
SomeThing(){
singleton = new Singleton();
}
public Singleton getInstance(){
return singleton;
}
}
上面的类Resource是我们要应用单例模式的资源,具体可以表现为网络连接,数据库连接,线程池等等。
获取资源的方式很简单,只要 SomeThing.INSTANCE.getInstance() 即可获得所要实例。下面我们来看看单例是如何被保证的:
首先,在枚举中我们明确了构造方法限制为私有,在我们访问枚举实例时会执行构造方法,同时每个枚举实例都是static final类型的,也就表明只能被实例化一次。在调用构造方法时,我们的单例被实例化。
也就是说,因为enum中的实例被保证只会被实例化一次,所以我们的INSTANCE也被保证实例化一次。
说了这么多,单例终究是一种设计模式,既然是设计模式,那就不是仅仅拘泥于哪一种写法,重要的是理解其根本思想,然后运用到框架设计,实际开发当中。我们知道Android 源码中也有很多地方使用了单例模式,最典型的就是获取系统服务这一块了(Context.getSystemServices(“”))。
具体的代码在这儿就不分析了,在网上找到了一个简化例子,更方便大家进行理解:
// 使用容器实现单例模式
public class SingletonManager{
private static Map map = new HashMap();
// 私有的构造方法
private SingletonManager(){}
// 开机之后,系统自动将配置自动启动的这些Service添加到管理类中
public static void registerService(String key,Object instance){
if(!map.continsKey(key)){
map.put(key,instance);
}
}
// 用户调用方法来取用
public static Object getService(String key){
return map.get(key);
}
}