面试篇:单例模式速食篇

我:面试管,你好。

面试官:你好,xxx么?。先自我介绍下?

我:我是一位靓仔。

面试官:无了?

我:嗯,无了。

面试官:... ...

我:......

许久之后......

面试官:那这样吧,我问你一些问题吧。你对叙利亚的局势怎么看。不好意思,说错了,你了解设计模式么?

我:嗯,了解

面试管:那你说说你用过了哪些设计模式吧。

我:单例模式、原型模式、工厂模式、建造者模式、代理模式、策略模式等等

面试官:说下你对单例模式的理解

我:

单例模式呢,在官方的定义上,是指,单例是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点。

在设计的时候,需要隐藏所有的构造方法。

ServletContext、ServletConfig、ApplicationContext都是单例模式来的。

PS:先说点定义,再举一些例子给面试官,让他觉得你平时开发过程是对这些类有一定的认知的,然后再等待面试管的提问。

 面试管:不错,那你知不知道单例模式有哪几种写法

我:

常见的写法:有饿汉式单例、懒汉式单例、注册式单例、ThreadLocal单例

PS:说出这些写法后,就不说了,等面试官问,那他下面肯定会问:区别、优缺点、平时怎么选择。

面试管:都有什么区别?优缺点是什么?(果不其然,面试官开始上钩了)

接下来你就开始个人秀了。

我:先思考一秒吧,假装在整理知识点

饿汉式单例

        在单例类首次加载的时候就会创建单例。

优点:执行效率高,安全,不需要任何的锁。
缺点:资源浪费,单个情况下还好,如果存在成千上万个对象。那么全部使用饿汉式就非常浪费资源。

 懒汉式单例

        懒汉式单例是在被外部调用的时候才创建单例,懒汉式又可以区分为简单懒汉式、双重检查懒汉式、静态内部类方式。

简单懒汉式

优点:节省了内存,

缺点:线程不安全。原因在于并发环境下,会存在第一个线程判断完后,开始初始化了,然后第二个线程又进来了。此时第一个线程还没完全初始化,所以instance是空的,那么第二个线程又开始了初始化,那么会容易造成多个对象了,不符合单例的原则了。

双重检查懒汉式

优点:线程安全、性能提高、只有在第一次空的时候才会阻塞,后续都不会阻塞了。双重检查上必须加上volatile关键字。

注意:当你说了volatile之后,面试管可能会被你引导到说volatile的作用,特性等等。今天讲单例,那么我就假设面试官不走这条道。

缺点:代码可读性差,难度大。 

静态内部类方式

优点:写法优雅、很好的利用了Java本身的语法特点,性能高、避免内存浪费

缺点:能够被反射破坏。给自己挖个坑,暂时不填,下面面试官一定会问

注册式单例

        注册式单例是将每一个实例都缓存到统一的容器中,使用唯一标识获取实例。注册式单例又可以区分为枚举类单例、容器类单例。 

枚举类单例

优点:代码优雅、线程安全、性能较高。还可以防止反射破坏(有坑:为什么可以防止反射破坏,留给面试官再问你)。

缺点:会存在资源浪费,单个对象情况下还好,如果存在成千上万个对象。那么全部使用枚举也会造成浪费资源,以及反序列化破坏单例的问题(有坑)。

容器类单例 

优点:性能较好、避免内存浪费
缺点:线程安全问题,以及反序列化破坏单例的问题

ThreadLocal单例

        threadLocal比较特殊,在同一线程内,获得的对象是相同的。对于不同的线程来说,ThreadLocal获得的对象都是不一样的,保证了线程内部的全局唯一、且天生线程安全。 

我:说了这么多,停顿下,然后拿起旁边的水杯,喝一口先。

怎么选择呢

        先思考一下,再说。

  • 如果程序不是很复杂,单例对象又不多,推荐使用饿汉式单例。
  • 如果经常发生多线程并发情况下,推荐使用静态内部类和枚举类单例。

我:全部说完后,再带上一句我就知道这么多了...拿起旁边的水杯,喝一口先。

面试官:嗯,很不错。你刚刚说到静态内部类的时候,能够被反射破坏,知道为什么么

这题考察的是你对反射的理解

我:

反射是指在程序运行时动态加载类并获取类的详细信息,从而可以操作类或对象的属性和方法。本质是JVM得到class对象之后,再通过class对象进行反编译,从而获取对象的各种信息。反编译后会从新创建一个新的对象。

面试官:嗯,不错,那你知道怎么防止么?

我:

嗯,知道。应急情况下,可以直接在构造方法中检查单例对象,如果已经存在,则抛出异常。

第二种呢,就是改造成枚举单例

面试官:枚举单例为什么可以防止被反射破坏?

我:

因为这是JDK底层决定的,JDK底层不允许用反射API调用枚举类,JDK在在Constructor类做了判断,明确规定了类的修饰符如果是枚举类,就抛出异常。

if ((clazz.getModifiers() & Modifier.ENUM) != 0)
            throw new IllegalArgumentException("Cannot reflectively create enum objects");

面试官:那你知道JDK为什么这么设计么?

我:不知道,各位看官帮忙留言评论下。

面试官:嗯,没关系。那么再问你,你之前说过,用枚举类也会有一些问题,比如资源浪费,以及反序列化破坏单例的问题,那我想知道,反序列化破坏单例的过程,可以说说么?如何解决的?

我:嗯,可以的,没问题。

反序列化的对象会重新分配内存,相当于重新创建对象。一般序列化会将对象从内存写到磁盘上,反序列化即是将对象从磁盘上加载到内存中,这个对象会在内存中重新分配空间,那么就是一个新的对象了

解决方案:在类中重写readResolve()方法,将返回值设置为单例对象

面试官:为什么重新readResolve()方法就可以解决

我:

原因是反序列化的时候,会调用ObjectInputStream类,这个类里面做了判断,如果存在readResolve方法,那么就调用该方法,并将方法内的返回值进行返回。

面试管:嗯,说的很好。小伙子什么时候可以来上班?

我:一个月之内吧。

面试官:可以提前么?

我:我考虑一下吧。

面试官:嗯,那行。那今天先这样吧?后续有需要再谈。

 我:好的,再见

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值