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

Posted learnjiawa

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了爱上面试的凑弟弟--你再问我单例模式试试?相关的知识,希望对你有一定的参考价值。

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

本书人物及背景:
小豪: 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~

以上是关于爱上面试的凑弟弟--你再问我单例模式试试?的主要内容,如果未能解决你的问题,请参考以下文章

求求你,下次面试别再问我什么是 Spring AOP 和代理了!

面试官,不要再问我快速排序了!

求求你,面试官,别再问我Redis啦!!!

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

拜托,面试别再问我基数排序了!!!

别再问我怎么准备C++面试了!