《张三求职日记》基础篇--(8) 社招也问单例模式?

567 阅读12分钟

写在前面:如果你是对技术有一定追求的程序员,那么你一定或多或少接触学习过设计模式。一般而言,常见的设计模式我们分为23种,而这23种又能从很多维度分类,例如可分为创建型,结构型和行为型软件设计模式。注意了,这里我们说的设计模式都是处于面向对象领域的,在分布式计算,企业应用软件及面向服务体系解构等领域中也有它们对应的设计模式。那么回到我们说的23种常见的设计模式中,设计模式其实也并不是什么高大上的东西,我们可以把它理解为是一种有益的技巧,学习它帮助我们开发出更易于复用和扩展的软件。而在其中,单例模式是代码量最少,结构最简单的设计模式,也是校招面试中考察频率非常高的设计模式,而在社招中,如果要考察的话,还有哪些我们不知道的点需要注意呢?有关于张三的背景介绍参考: 张三背景介绍

张三又开始了新的一轮面试。

面试官:先来个自我介绍吧。

张三:!@#¥%……&*。

前面的一阵交谈此处省略

面试官:我看你简历里提到了设计模式,能讲讲你们项目中用到了哪些设计模式吗?

张三:我们的订单的状态扭转用到了状态模式和状态机模式,下单流程中用到了责任链模式,代码中还有些地方用到了代理模式,我有印象的应该就这么多了。

面试官:那你讲讲你们的状态模式和状态机模式怎么用的吧。

张三:状态模式啊,首先……(由于我们本次讲的是单例模式,其他的设计模式略过)

面试官:单例模式知道吗?

张三心想,单例模式肯定知道啊,虽然之前没用到,但是这个最简单的铁定牢牢记在心里的,再而言之,当年校招我被这个虐过,我既然来面试了,肯定做好准备了。

张三:单例模式当然知道,使用它可以帮助我们创建对象时,可以创建出唯一的一个实例。然后根据创建实例时机的不同,又能够分为懒汉式和饿汉式,当然还有很多像什么双检锁这样的衍生的类型。

面试官:你刚刚说到了懒汉式和饿汉式,那么它们是线程安全的吗?

张三心想,这些代码我都能随手写出来,问这个能难倒我?

张三:懒汉式由于它很懒,创建对象时是调用方法时创建的,而如果再多线程环境中,由于判断对象是否为null的方法有可能判断的不正确,导致有可能创建了多个实例,所以它是线程不安全的。而饿汉式一上来就创建了一个实例,所以不存在并发线程安全问题。

面试官:那为了避免懒汉式的线程安全问题,我们有什么办法呢?

张三:加锁就能够解决线程安全的问题,我们只需要对我们获取这个对象的方法加一把synchronized锁,我们就能够保证创建的对象是唯一的。

面试官:那有没有不加锁的办法让懒汉式单例模式线程安全呢?

张三一脸的问号,不加锁?懒汉?线程安全?双检锁吗?不对啊,双检锁也加锁啊,这什么问题啊。

张三:我暂时想不到还有什么办法。

面试官:没事,我看你简历上还写了……

于是面试官又换了个问题继续问,可以看出,虽然张三已经回答到的点已经很多了,但是这里还是栽了个跟头,有点难受。后面的问题省略。

面试官:这次的面试就先到这里,后面会有我们的HR联系你。

张三:好的……


面试中考察设计模式究竟是在考察什么呢? 说句实在话,除非是专门研究设计模式的人,不然绝大部分人也是没有什么可能把设计模式都搞明白的。每个人负责的开发场景都不可能面面俱到,能用得上的设计模式可能都只是个位数,再考虑到开发效率以及因为不熟悉对风险的规避考虑,没有多少人写过大量的设计模式。那么回到之前的问题,究竟是在考察什么?

考察设计模式就和考察算法一样,一方面设计模式确实是能够区别程序员等级的重要技能,因为天天写增删改查的程序员和天天想着怎么用设计模式重构代码的程序员显然所处的岗位和项目所处的时期是不同的,如果想进入一家业内领先的公司,设计模式这项技能肯定是要去了解的。另一方面,就比如说平常练习的不到百行代码的复杂算法好像和开发中动则输出上千行代码好像区别很大,好像平常写起来那些算法根本用不到,但是仔细一想,是不是平常练习的算法潜移默化的影响了我们的编码呢?当练习到了一定程度后,哪怕是开发业务代码,也会开始思考什么时候该用什么集合容器,写出的代码会不会有时间复杂度空间复杂度的优化空间。设计模式也是一样的,当我们知识足够丰富的时候,我们在开发过程中就会不自主的思考我们的代码中是不是会有冗余需要优化,是否符合开闭原则,有多少代码可以复用,系统的耦合度如何,代码如果需要增加功能会影响多少原有代码等等。可以说如果对设计模式有系统的学习的话,自己的编程思想会有比较大的提升。

然后我们再来回来看张三的遇到单例模式的考察。前面也说到了。单例模式可以说是最简单的设计模式了,应该是唯一一个仅仅只有一个类就实现的设计模式了,正是因为它简单,拿来考察我们是否学习过设计模式是再合适不过的了。所以校招的话如果要问到了设计模式,手写一个单例模式应该是需要准备应对的内容。然后我又说到了它简单,简单在哪呢?是不是谁都能写出来呢?哪怕半年一年没再看过是不是也能写出来呢?答案是是的,只要学的时候是带着思考和理解学的,写这么一个设计模式是非常简单的。

因为设计模式的本质是对某些问题的经验性的总结的解决方案,所以我们也可以大胆的发挥自己的想象来自己创造代码,但是考虑到学习代码本来就是一个循序渐进的过程,可以先看别人是这么写的,然后带着思考把他的写出来,当自己能想的很清楚了,也可以说是学会了。

那我们就来带着思考写一把单例模式。

首先,我们要想想单例模式是为了解决什么问题?单例模式属于创建型模式,这一类型的模式都有一个特点,那就是能在创建对象时隐藏创建逻辑的方式,而不是使用new来实例化对象。而单例模式需要创建的对象只有一个实例,我们每次取到的都是这个实例。

那么我们把需要做的事情想清楚了,那就来一步一步分解就行了,我们先来以懒汉式单例模式举例。我们假设我们需要创建的对象的类名叫做Singleton,那么我们如果想要别人只能生成唯一的一个实例,首先我们要做的,就是不能让别人能够随随便便通过new方法就能创建对象实例吧。所以我们的第一步就是私有化我们的构造方法:

private Singleton (){
}

如果不私有化构造方法,会有一个默认的public的无参构造方法这一点大家应该知道吧。

           

然后因为我们私有化了构造方法,别人想取对象该怎么取呢?那我们得提供一个方法来让别人调用,取到实例,所以这个方法肯定是公有的,这个方法还得是静态方法,能够让别人直接调用的,所以方法的外部为

public static Singleton getInstance() {
}

那么我们举的例子为懒汉式,懒汉式的意思就是我们要等别人调用这个方法的时候才生成实例,而要生成的实例只会有一个,怎么办呢?判空呗,所以方法里面的代码为:

if (instance == null) {
    instance = new Singleton();
}
return instance;  

在方法外声明我们要实例的对象变量(因为是静态方法,所以对象也是静态的)

private static Singleton instance;

所以,完整的懒汉式单例模式的代码为

public class Singleton1 {
private static Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
    if (instance == null) {
        instance = new Singleton();
    }
    return instance;
    }
}

怎么样,通过一步一步的分析,是不是感觉很简单?

但是这个方法在多线程环境下是不安全的,原因就是因为当两个线程都是第一次进来instance判断为空的时候,就有可能创建了两次实例了,为了避免这个问题,可以在getInstance方法上加上一个synchronized关键字,这样就能保证懒汉式的单例模式的线程安全了。

public static synchronized Singleton getInstance() {
}

但是这样仍然不好,因为synchronized太重了,仅仅只为了第一次可能出现的问题就加这个关键字,有点得不偿失。那还有什么办法呢?

既然我们只有在第一次判断为空生成实例的时候有可能出现线程安全问题,那我们就在判空那里加锁就好了呀,所以代码为:

if (singleton == null) {
    synchronized (Singleton.class) {
        if (singleton == null) {
            singleton = new Singleton();
        }
    }
}

然后为了保证第二次判空的singleton能够判断正确,我们需要在声明时给singleton加一个volatile关键字保证可见性(有关于volatile的作用后面出一期讲)。也就是声明的代码为:

private volatile static Singleton singleton;

这样我们既解决了多线程的安全问题,又解决了效率问题,这个代码一步一步分析出来后还是比较简单的,如果能在面试中当场写出这样的代码,还是很加分的,毕竟不仅对单例模式很了解,有对多线程有一定的认识。这个就是比较有名的双检锁单例模式。

那么之前最后问倒张三的是不加锁怎么保证懒汉单例模式线程安全,这个确实一般人没有注意过,不过在说这个问题之前,先把饿汉稍微提一提。

饿汉式单例模式非常简单,在声明对象时就把对象初始化好了就行了:

    private static Singleton instance = new Singleton();
    public static Singleton getInstance() {
        return instance;
    }  

然后每次取对象直接返回实例就好了,不过由于这是静态对象静态方法,在类加载的时候就初始化实例了,有可能生成的对象根本用不到浪费内存,不过它确实也是线程安全的。

那么不加锁的懒汉式线程安全到底是什么呢?

这个说难也不难,说简单也不简单,当知识储备很充足的时候也是可以想的倒的,但是一般人不看上个两三遍可能都看不明白。

思路就是在Singleton类中加上一个静态内部类,在静态内部类中创建实例出的instance对象,调用getInstance()方法时取静态内部类中的实例出的对象就行了,而且它也确实是懒加载的,代码还是比较简单的:

public class Singleton {
private static class SingletonHolder {
    private static Singleton instance = new Singleton();
    }
    private Singleton() {
    }
    public static Singleton getInstance() {
        return SingletonHolder.instance;
    }
}

不过张三没有回答出来确实也不怪他,这个还是一个知识储备的问题,现在我们明白了这是怎么一个回事,我们后面也就会回答了,而且如果写出这个,是不是更加分呢?但是这个说是也有不安全的问题,就是当遇到反射攻击和反序列化攻击时,它依然可能生成不止一个实例(暂时不展开了)。

最后再提一下,可能最棒的单例实现模式就是枚举了,但是也要看具体的使用场景,代码如下:

public enum Singleton {
    INSTANCE;
    public void doSomething() {
    System.out.println("doSomething");
    }
}

小小一个单例模式,也能引出这么多知识,如果你之前对单例模式完全不了解,相信看完这篇后也能随手写出一个单例模式了,甚至可能写双检锁也是能写出来的。如果之前有一定的了解,相信这次也了解到了许多新知识。

张三在这次的面试中收获颇丰,我们下周见。