面试时的那些坑之单例模式

我们搞技术的,面试不免要问到设计模式,而其中出场率最高的就是今天要说的单例模式了。
在这儿分享一个我面试的故事吧。

我的面试故事

当时是去一家较小的互联网公司,工资开的挺高,我想要求自然也不会很低。进去之后,面试官和我印象中的消瘦格子衫程序员完全不一样,高大偏胖,眼神犀利,有种不怒自威的感觉,不免有些紧张。
前边都是问一些项目经历,一些基础知识,答得还算可以,心态也慢慢的平静下来。到最后问到设计模式,我放下的心又立即悬了起来,我想糟了,除了单例,其它的虽然看过一些书,但是没有怎么实际用过,当然也说不出什么,在大神面前不是班门弄斧吗?就告诉他,我实际项目中只用过单例。他就让我手写一下单例模式,我心中大喜,单例在网上看过,默写没有什么问题。接过纸,刷刷几笔,就将双重检查锁定单例(文章下边会有讲)写了出来,因为百度单例模式,弹出的博客最终几乎都会将这种写法作为最终的也是最好的写法。我很自信,眼神中流露出几丝得意。他看了一下,眉头紧皱,我有点儿不解,也不敢贸然去问为什么,只好默不作声看着他。大概过了五六秒,他指着我的代码,问我,还有更好的写法吗?我不解,心想,这不就是最好的写法吗。我以为他没有看懂,忙指着代码给他解释。他不耐烦的打断了我,继续追问,你平时写单例就是这么写的吗?我心里直犯嘀咕,默默的点点头。但我到此时还是坚信,我写的没有问题,咽了口唾沫,尝试轻声问道,那应该怎么写?他显然已经不想回答了,说我送你下去吧。我收拾好简历,垂头丧气的跟着他下楼了,但我依旧不死心,问他,我到底差在哪儿?应该怎么去提高?他转过头,愣了一下,然后笑了一下,说,基础还可以,但是项目经验太少了,公司要求比较高,你不要拘泥于一些网上的demo,去 github 上找一些完整的项目,仔细研究一下。然后他就转身离开了。
后来仔细想想,他说的很对,但是这个单例模式的问题一直困扰了我好久。
因为我是做 Android 系统二次开发,有一天,突然想分析一下源码中使用的设计模式,凑巧,这个谜团就解开了,于是就有了这篇博客。

1、什么是单例模式?在什么情况下需要使用单例模式?

确保某一个类只有一个实例,并且自行实例化并向整个系统提供这个实例。在有线程池,网络操作,缓存操作等很耗费资源的对象,我们不希望构造多个这样的实例,所以需要单例模式。

2、常用的单例模式写法。

//饿汉单例模式
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();这句来说,会被编译成三条汇编指令

  1. 给 Singleton 分配内存
  2. 调用构造函数,初始化成员字段
  3. 将singleton 对象指向分配的内存空间(执行完这一步,singleton != null)

而 java 编译器允许处理器乱序执行,所以上述三条汇编指令的执行顺序可能并不是我们理解的 1-2-3 这么简单,也有可能是 1-3-2。如果是后者,就会出错了。看图:

面试时的那些坑之单例模式_第1张图片

谷歌也意识到了这个问题,调整了 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也被保证实例化一次。

3、单例模式在Android源码中的体现

说了这么多,单例终究是一种设计模式,既然是设计模式,那就不是仅仅拘泥于哪一种写法,重要的是理解其根本思想,然后运用到框架设计,实际开发当中。我们知道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);
    } 
}

你可能感兴趣的:(设计模式)