爱上面试的凑弟弟--你再问我单例模式试试?

本系列博客以情景对话形式,用一个又一个的小故事或者编程实例来组织,对于实际开发尤其是面试中经常遇到的知识点进行深入探讨。

本书人物及背景:
小豪: 23岁,武汉某双非本科不知名专业大学四年级学生,成绩一般,面临毕业,对后端开发、Java很感兴趣,正求职找工作。
宇哥: 跟小豪通过租房认识,两人是室友,26岁,毕业后长期从事软件开发工作,是一个半吊子工程师,兴趣爱好是吹牛,不打草稿那种。

1.1 面试失败

小豪热爱编程,觉得写代码、做网站开发无敌酷炫雕炸天,在某站上看完了某马基础班和就业班的教学视频,觉得自己已经成为了斗宗强者,踌躇满志,海投简历,终于被某单位临幸,欣喜若狂,欣然赴约。
面试官小哥哥长的十分秀气,穿着格子衬衫,理着精神的小平头,戴着圆框眼镜。小豪心里大定,感觉多了几分把握。

面试官:先自我介绍一下,重点说你做过的项目。

小豪心里一乐,嘿嘿,昨晚刚刚背过的,一阵摇头晃脑过后……

面试官: 还不错,你刚刚提到了单例模式,你对单例模式了解多少?

小豪:(吞吞吐吐…)常见的单例模式有饿汉式懒汉式?(不确定的疑问口气,企图得到面试官回应ing)

面试官微微一笑:你可以试着手写一个饿汉式吗?

小豪面色一僵: 不太记得了

面试官:你能说说饿汉式和懒汉式有啥不同吗?

小豪:饿汉式就是比较饿了,想快点吃炸鸡腿儿?

面试官微笑:出去帮我关一下门。

小豪拿着背包落荒而逃……

1.2 啥都懂一点儿的宇哥

下班回到家的宇哥看见小豪对着一本砖头一样的书籍在发呆……
宇哥:看啥呢,像个二愣子。
小豪:四人帮的23种设计模式,今天面试官问我单例模式了,我人都傻了。
宇哥:单例模式在开发中很常用,你研究下单例模式不就好了。
小豪:你教我呗宇哥,我看书看了半天也看不懂。
宇哥:行啊,刚好我懂一点儿。概念我就不跟你说了,你自己背吧,我跟你说点有意思的,这样,你先默写一个饿汉式的例子。
小豪:这个我会,我特意记了的。
说着小豪就在电脑上用idea很快敲出了代码。

public class Hungry {    // 1    public Hungry(){    };    // 2    private final static Hungry hungry = new Hungry();    //3    public static Hungry getInstance(){        return hungry;    }

}

宇哥:不错不错,骚年好快的手速(流氓夸奖)。不过你知道这为啥叫单例模式,为啥要叫饿汉式不?
小豪:面试官也问过我,我没答上来。

宇哥:光背代码你可当不了程序员,嘿嘿,我用粗鄙的话给你解释一下,你听懂了,以后就不会忘了。首先,记号1的代码是一个私有化的空构造器,记号2的代码是构造一个Hungry实例对象,记号3的代码是一个静态的get方法,返回前面的对象。这三行代码共同作用就是你在外面想创建Hungry对象的时候,用new的方式是做不了的,因为构造器是私有的、空的,只能调用那个get方法,返回前面已经创建好的hungry对象…

小豪:哦哦哦,我懂了懂了,而且因为记号2的代码用final和static修饰过了,所以有且只有一个,且不能修改hungry对象地址了?

宇哥:嘿,还学会抢答了,那你知道为啥要分成饿汉式懒汉式吗,臭弟弟。
小豪:我想想,是不是因为当初始化这个Hungry类的时候,还没有调用get方法,成员对象就创建了,有点浪费空间
宇哥:对,生产环境下饿汉式确实存在一点小问题,所以才有了懒汉式,先声明一下对象为null,等你真正调get方法使用的时候,我再在方法里面创建一个实例对象。我打给你看

public class LazyMan {    private LazyMan(){};    private static LazyMan lazyMan;    public LazyMan getLazyMan(){        // 1        if(lazyMan == null){            lazyMan = new LazyMan();        }        return lazyMan;    }}

小豪:哇,通透了,加个判断条件,如果当前对象为空,就创建一个给它,否则就直接返回,这样可以保证单例模式。宇哥nb!

宇哥:呵呵,你啊,难怪面试老不行,这个问题才刚开始有意思起来,要不然你以为为啥面试官就爱问这个。你想想如果是并发环境下,这样写的代码安不安全,一定是单例模式吗还?
小豪:wc,你们都这么阴的吗……你别说,还真不是线程安全的,并发条件下多个线程可以几乎同时通过if判断条件,最后弄出两条人命,是不是要给房门加把锁?
宇哥:你说话就说话,开车干嘛,确实要加同步锁。你看

public class LazyMan {    private LazyMan() {    }

    private static LazyMan lazyMan;

    public static LazyMan getInstance() {        if (lazyMan == null) {            synchronized (LazyMan.class) {                if (lazyMan == null) {                // 记号1                    lazyMan = new LazyMan();                }            }        }        return lazyMan;    }}

小豪: 啊啊啊,这不就是传说中的双重检查吗我滴龟龟,我懂了,我懂了!

宇哥:瞧你那样,比我还骄傲。要不我们今天就学到这。
小豪:你嘴角那诡异的笑容是怎么回事儿。你是不是藏私了?宇哥~

宇哥:嘻嘻,被你发现了,其实这个问题如果你和面试聊到这只能说明你了解了基本的单例模式,但是,这个问题远远没有这么简单。这个双重检查,其实还是不够安全

小豪:真的吗?还能出什么问题?

宇哥:这涉及到了原子性指令重排的问题,你看,记号1处的那一行代码不能保证原子性,要想执行完,至少有三个步骤。

  1. 分配内存
  2. 执行构造方法
  3. 指向地址
    假设有两个线程A和B,线程A执行该语句由于指令重排没有执行第2步,而是先执行了第3步,这个关键的时刻线程B来了,一判断,哎呀妈呀这不有了lazyMan对象了吗,我直接用了哦。溜了溜了。但事实上B拿走的其实是一个不完成的lazyMan,后面使用它时会出现意料之外的错误。

小豪:宇哥,我感觉我听你说完,我整个人升华了,真的,这么几行程序你能说上一个小时,真的有点东西,我对这个计算机底层的东西越来越有兴趣了。那要怎么避免你说的这个问题啊,都已经给它加了同步锁了,她还出去偷人,我们还看不到,抓不到现场。

宇哥:嘻嘻,给她房间安装一个监控就完事了嗷。你还记得你学过一个关键字叫volatile吗老弟。

小豪:龟龟i,想起来了,volatile关键字,保证共享变量在内存中的可见性,还能避免指令重排,原来是这么用的,我以前都不懂它是干嘛的,只知道是轻量级的synchronized锁。我来给它加上,嘻嘻。

public class LazyMan {    private LazyMan() {    }        //改动处    private volatile static LazyMan lazyMan;

    public static LazyMan getInstance() {        if (lazyMan == null) {            synchronized (LazyMan.class) {                if (lazyMan == null) {                    lazyMan = new LazyMan();                }            }        }        return lazyMan;    }}

宇哥:改的不错哦,小豪还是可以的。
小豪:宇哥,你别阴阳怪气的像个阴阳人,你是不是还有话说。
宇哥:我肚子饿了。
小豪:我给你点外卖,桥头排骨,你快说。

宇哥:你知道反射吗,问你概念,你肯定知道。我告诉你,反射是Java里面的九阳神经,霸道无比,用反射创建对象,可以无视private修饰的方法,直接在类的外面newInstance,将你前面写的什么这个式那个式的,统统干趴下,一句话,在反射面前,单例模式,不堪一击。

小豪:真的假的啊,我没试过,我敲敲看,搞两个对象比较一下它们的hashcode,看单例模式还有没有用。

class FuckSinleton{    public static void main(String[] args){

        try {            //用优化过的懒汉式单例模式创建一个lazyMan对象            LazyMan lazyMan1 = LazyMan.getInstance();            //用反射创建一个单例模式            Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);            declaredConstructor.setAccessible(true);            LazyMan lazyMan2 = declaredConstructor.newInstance();            //比较两个对象的hashcode,看是不是同一个,么么大            System.out.println(lazyMan1.hashCode());            System.out.println(lazyMan2.hashCode());            System.out.println(lazyMan1 == lazyMan2);

        } catch (Exception e) {            e.printStackTrace();        }

    }}

小豪的控制台输出如下:


小豪:还真是不一样了,生了两个,龙凤胎,我的天,这反射可真滴秀!
宇哥:哈哈哈,你知道这一点,以后面试在被问到对单例模式的了解时,你就可以吹牛逼了。

小豪:有道理。不过,宇哥,山人有一妙计可以解决这个问题,我可以在私有的构造方法里面防守一波,不让反射为所欲为。

public class LazyMan {    private LazyMan() {      //既然反射是破坏私有构造方法进来的,那就在这里防守一波      /**         * 在私有的构造函数中做一个判断,如果lazyMan不为空,说明lazyMan已经被创建过了,         * 如果正常调用getInstance方法,是不会出现这种事情的,所以直接抛出异常!         * */        synchronized (LazyMan.class) {            if (lazyMan != null) {                throw new RuntimeException("不要试图用反射破坏单例模式");            }        }    }

    private volatile static LazyMan lazyMan;

    public static LazyMan getInstance() {        if (lazyMan == null) {            synchronized (LazyMan.class) {                if (lazyMan == null) {                    lazyMan = new LazyMan();                }            }        }        return lazyMan;    }}


宇哥:哎哟,小坏蛋还真被你抓住了,可以啊。不过你以为反射就这点能耐吗,你防守不住的,上面的代码中在用反射之前第一个lazyMan对象你是通过调getInstance实现的,所以导致反射创建对象时你在私有构造函数里面的判断起作用了,不过如果我两个lazyMan都直接用反射来创建,你的防守就没用了哦。

public static void main(String[] args) {    try {        Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);        declaredConstructor.setAccessible(true);        LazyMan lazyMan1 = declaredConstructor.newInstance();        LazyMan lazyMan2 = declaredConstructor.newInstance();        System.out.println(lazyMan1.hashCode());        System.out.println(lazyMan2.hashCode());    } catch (Exception e) {        e.printStackTrace();    }}

单例模式3图
宇哥用啃了桥头排骨沾了油的手按下回车,头一甩,厚厚的发量,绝对自信:你看,还是超生了,单例模式,不堪一击!

1.3 充满信心的小豪

小豪:有点东西啊宇哥,这不无解了。
宇哥:还有办法防守,比如上面的构造方法里增加一个标志量,或者使用枚举类写单例模式
小豪:啊啊,你快说,我上瘾了,我要学!我要LearnJava!

宇哥:你可拉倒吧凑弟弟,几点了都,改天的吧,其实还有单例模式还可以用静态内部类写哦,不过今天讲太久了,你先消化一下,我们日后再战,日后再战,嗝~。

小豪:ojbk,今天确实搞懂了不少,我得总结一下,下次你一定要跟我说完,么么哒宇哥!你最牛逼!下次面试我一点都不虚了。

宇哥摆了摆手,哼着歌儿洗澡去了,拜拜了您呐。

画外音

兄弟萌,妹妹萌,这个系列准备用心写故事,用心学技术,写的不好多包涵保函。

LearnJava,冲呀,想看就私信评论留言催更,撩我啊,凑弟弟,凑妹妹们!

参考文献

[1]程杰. 大话设计模式 [M].清华大学出版社,2007年01月01日.

更多

对我的文章感兴趣,欢迎查看我的其他系列博客《Java牛客网剑指offer编程题》《跟凑弟弟一起修炼集合框架》《Java多线程大闯关》《Java IO流大闯关》《数据库和Redis》持续更新中……
关注微信公众号LearnJava:一起学习,一起进步!skr~

原文地址:https://www.cnblogs.com/learnjiawa/p/12004636.html

时间: 2024-10-15 16:01:30

爱上面试的凑弟弟--你再问我单例模式试试?的相关文章

【深度】扒开V8引擎的源码,我找到了你们想要的前端算法(下次面试官再问算法,用它怼回去!)

算法对于前端工程师来说总有一层神秘色彩,这篇文章通过解读V8源码,带你探索`Array.prototype.sort`函数下的算法实现. 来,先把你用过的和听说过的排序算法都列出来: * 快速排序 * 冒泡排序 * 插入排序 * 归并排序 * 堆排序 * 希尔排序 * 选择排序 * 计数排序 * 桶排序 * 基数排序 * ... 答题环节到了, sort 函数使用的以上哪一种算法? 如果你在网上搜索过关于 sort 源码的文章,可能会告诉你数组长度小于10用插入排序,否则用快速排序. 开始我也是

面试官,不要再问我“Java GC垃圾回收机制”了

Java GC垃圾回收几乎是面试必问的JVM问题之一,本篇文章带领大家了解Java GC的底层原理,图文并茂,突破学习及面试瓶颈. 楔子-JVM内存结构补充 在上篇<JVM之内存结构详解>中有些内容我们没有讲,本篇结合垃圾回收机制来一起学习.还记得JVM中堆的结构图吗? 图中展示了堆中三个区域:Eden.From Survivor.To Survivor.从图中可以也可以看到它们的大小比例,准确来说是:8:1:1.为什么要这样设计呢,本篇文章后续会给出解答,还是根据垃圾回收的具体情况来设计的.

面试linux运维一定会问到Shell脚本这24个问题

面试linux运维一定会问到Shell脚本这24个问题 虽然现在Python在运维工作中已经使用很普遍,但是很多企业在找Linux云计算工程师的时候还是会问到 shell 脚本的问题,它有助于你在工作环境中自动完成很多任务. 如下是一些面试过程中,经常会遇到的 shell 脚本面试问题及解答: Q:1 Shell脚本是什么.它是必需的吗? 答:一个Shell脚本是一个文本文件,包含一个或多个命令.作为系统管理员,我们经常需要使用多个命令来完成一项任务,我们可以添加这些所有命令在一个文本文件(Sh

[转帖]别再问“分库分表”了,再问就崩溃了!

别再问“分库分表”了,再问就崩溃了! https://www.cnblogs.com/butterfly100/p/9034281.html “ 在谈论数据库架构和数据库优化的时候,我们经常会听到分库分表,分库分表其实涉及到很多难题,今天我们来汇总一下数据库分库分表解决方案. 图片来自 Pexels 数据切分 关系型数据库本身比较容易成为系统瓶颈,单机存储容量.连接数.处理能力都有限. 当单表的数据量达到 1000W 或 100G 以后,由于查询维度较多,即使添加从库.优化索引,做很多操作时性能

面试官再问你 HashMap 底层原理,就把这篇文章甩给他看

前言 HashMap 源码和底层原理在现在面试中是必问的.因此,我们非常有必要搞清楚它的底层实现和思想,才能在面试中对答如流,跟面试官大战三百回合.文章较长,介绍了很多原理性的问题,希望对你有所帮助~ 目录 本篇文章主要包括以下内容: HashMap 的存储结构 常用变量说明,如加载因子等 HashMap 的四个构造函数 tableSizeFor()方法及作用 put()方法详解 hash()方法,以及避免哈希碰撞的原理 resize()扩容机制及原理 get()方法 为什么HashMap链表会

面试官求你了,别再问我TCP的三次握手和四次挥手

少点代码,多点头发 本文已经收录至我的GitHub,欢迎大家踊跃star 和 issues. https://github.com/midou-tech/articles 三次握手建立链接,四次挥手断开链接.这个问题算非常经典的问题,也是面试官非常喜欢问的问题. 不夸张的说,龙叔在校招面试的时候每一家公司都问到过关于三次握手和四次挥手相关的问题,相信大家也都差不多被面试官各种怼. 这个问题的重要性,已经意识到.不说废话了,接下来就是听龙叔给你安排的明明白白. 先画个图,看下TCP的建立连接 和

面试请不要再问我Spring Cloud底层原理

概述 毫无疑问,Spring Cloud是目前微服务架构领域的翘楚,无数的书籍博客都在讲解这个技术.不过大多数讲解还停留在对Spring Cloud功能使用的层面,其底层的很多原理,很多人可能并不知晓.因此本文将通过大量的手绘图,给大家谈谈Spring Cloud微服务架构的底层原理. 实际上,Spring Cloud是一个全家桶式的技术栈,包含了很多组件.本文先从其最核心的几个组件入手,来剖析一下其底层的工作原理.也就是Eureka.Ribbon.Feign.Hystrix.Zuul这几个组件

拜托!面试请不要再问我Spring Cloud底层原理

概述  毫无疑问,Spring Cloud是目前微服务架构领域的翘楚,无数的书籍博客都在讲解这个技术.不过大多数讲解还停留在对Spring Cloud功能使用的层面,其底层的很多原理,很多人可能并不知晓.因此本文将通过大量的手绘图,给大家谈谈Spring Cloud微服务架构的底层原理. 实际上,Spring Cloud是一个全家桶式的技术栈,包含了很多组件.本文先从其最核心的几个组件入手,来剖析一下其底层的工作原理.也就是Eureka.Ribbon.Feign.Hystrix.Zuul这几个组

以后面试官再问你三次握手和四次挥手,直接把这一篇文章丢给他

三次握手和四次挥手是各个公司常见的考点,也具有一定的水平区分度,也被一些面试官作为热身题.很多小伙伴说这个问题刚开始回答的挺好,但是后面越回答越冒冷汗,最后就歇菜了. 见过比较典型的面试场景是这样的: 面试官:请介绍下三次握手 求职者:第一次握手就是客户端给服务器端发送一个报文,第二次就是服务器收到报文之后,会应答一个报文给客户端,第三次握手就是客户端收到报文后再给服务器发送一个报文,三次握手就成功了. 面试官:然后呢? 求职者:这就是三次握手的过程,很简单的. 面试官:...... (番外篇: