当前位置:首页 > Java技术 > 最适合作为Java基础面试题之Singleton模式

最适合作为Java基础面试题之Singleton模式

2022年08月05日 11:36:15Java技术2

看似只是最简单的一种设计模式,可细细挖掘,static、synchronized、volatile关键字、内部类、对象克隆、序列化、枚举类型、反射和类加载机制等基础却又不易理解透彻的Java知识纷纷呼之欲出,让人不禁感叹Singleton单例模式是最适合作为考察应聘者Java基础的一道考题。
从表面上看,Singleton希望并限制该类的实例只能有一个,如JDK自带的Runtime类,其构造方式通常是一个private构造函数、static的该类实例、以及返回该实例的getInstance方法。

最适合作为Java基础面试题之Singleton模式 _ JavaClub全栈架构师技术笔记

那接下来我们就看看实现一个Singleton究竟有哪几种方式。

1. Eager Singleton

public class EagerSingleton {

    private static final EagerSingleton INSTANCE = new EagerSingleton();

    private EagerSingleton() {
    }

    public static EagerSingleton getInstance() {
        return INSTANCE;
    }
}

这种称为“饿汉式”的实现方式可能是最简单也最常见的,顾名思义,该实例在类加载的时候就会自动创建不管之后是否被使用。所以如果该类实例化的开销比较大,这种方式或许就不太理想,不过它的优点也很明显,即无需担心多线程同步获取该实例时可能出现的并发问题。

2. Lazy Singleton

public class LazySingleton {

    private volatile static LazySingleton INSTANCE = null;

    private LazySingleton() {
    }

    public static synchronized LazySingleton getInstance() {
        if (INSTANCE == null)
            INSTANCE = new LazySingleton();
        return INSTANCE;
    }
}

这种方式也有个形象的名字“懒汉式”,既然觉得类加载时就完成实例化有点浪费,那不如将这一过程推迟到实际需要使用时,可是在此值得注意的是为了避免多线程并发场景下可能导致的莫名其妙多创建出一个实例的弊端,getInstance方法必须标记为synchronized方法或采用synchronized代码块来加锁实现。但是这种过度保护的代价是非常高昂的,其实只有当该实例未被创建时才有必要加锁控制并发,因此更多时候是没必要同步的,此类方式并不经济划算。

3. Lazy Singleton with Double Check

public class LazySingletonWithDoubleCheck {

    private volatile static LazySingletonWithDoubleCheck INSTANCE = null;

    private LazySingletonWithDoubleCheck() {
    }

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

作为Lazy Singleton的改良版,这种采用了double-check的实现方式避免了对getInstance方法总是加锁。注意到尚未实例化时,存在两次检查的流程,第一次检查如果发现该实例已经存在就可以直接返回,反正则加类锁并进行第二次检查,原因在于可能出现多个线程同时通过了第一次检查,此时必须通过锁机制实现真正实例化时的排他性,保证只有一个线程成功抢占到锁并执行。此举即保证了线程安全,又将性能折损明显降低了,不失为比较理想的做法。

4. Inner Class Singleton

public class InnerClassSingleton {

    private static class SingletonHolder {
        private static final InnerClassSingleton INSTANCE = new InnerClassSingleton();
    }

    private InnerClassSingleton() {
    }

    public static final InnerClassSingleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

另外一种可以有效解决线程并发问题并延迟实例化的做法就是如上代码所示的利用静态内部类来实现。单例类的实例被包裹在内部类中,因此该单例类加载时并不会完成实例化,直到有线程调用getInstance方法,内部类才会被加载并实例化单例类。这种做法应该说是比较令人满意的。

以上就是比较常见的实现Singleton的方式,这也是一般的Java面试所涉及的深度。可是带有好奇心的人不禁会琢磨所谓的单例真的可以保证全局唯一性吗?能不能采用一些tricky 的方式去破坏这一核心属性呢?这才是本文着重介绍的部分,因为其覆盖了多个重要的知识点。接下来我们就一起看看通过哪些看似合法的手段可以有效绕开传统Singleton实现中仅靠将构造函数私有化达成的单例从而创建出多个实例。

1. Break Singleton with Clonable

public class ClonableSingleton implements Cloneable{

    private static final ClonableSingleton INSTANCE = new ClonableSingleton();

    private ClonableSingleton() {
    }

    public static ClonableSingleton getInstance() {
        return INSTANCE;
    }

    public Object clone() throws CloneNotSupportedException{
        return super.clone();
    }
}

没错,第一种比较容易想到的方式就是clone,Java中类通过实现Clonable接口并覆写clone方法就可以完成一个其对象的拷贝。而当Singleton类为Clonable时也自然无法避免可利用这种方式被重新创建一份实例。通过以下的测试代码即可检验通过clone我们可以有效破坏单例。

public static void checkClone() throws Exception {
    ClonableSingleton a = ClonableSingleton.getInstance();
    ClonableSingleton b = (ClonableSingleton) a.clone();

    assertEquals(a, b);
}

2. Break Singleton with Serialization

public class SerializableSingleton implements Serializable{

    private static final long serialVersionUID = 6789088557981297876L;

    private static final SerializableSingleton INSTANCE = new SerializableSingleton();

    private SerializableSingleton() {
    }

    public static SerializableSingleton getInstance() {
        return INSTANCE;
    }
}

第二种破坏方式就是利用序列化与反序列化,当Singleton类实现了Serializable接口就代表它是可以被序列化的,该实例会被保存在文件中,需要时从该文件中读取并反序列化成 对象。可就是在反序列化这一过程中不知不觉留下了可趁之机,因为默认的反序列化过程是绕开构造函数直接使用字节生成一个新的对象。于是,Singleton在反序列化时被创造出第二个实例。通过如下代码可轻松实现这一行为,a与b最终并不相等。

public static void checkSerialization() throws Exception {
    File file = new File("serializableSingleton.out");
    ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(
    file));
    SerializableSingleton a = SerializableSingleton.getInstance();
    out.writeObject(a);
    out.close();

    ObjectInputStream in = new ObjectInputStream(new FileInputStream(file));
    SerializableSingleton b = (SerializableSingleton) in.readObject();
    in.close();

    assertEquals(a, b);
}

3. Break Singleton with Reflection

public static void checkReflection() throws Exception {
    EagerSingleton a = EagerSingleton.getInstance();

    Constructor<EagerSingleton> cons = EagerSingleton.class
        .getDeclaredConstructor();
    cons.setAccessible(true);
    EagerSingleton b = (EagerSingleton) cons.newInstance();

    assertEquals(a, b);
}

前两种破坏方式说到底都是通过避免与私有构造函数正面冲突的方式另辟蹊径来实现的,而这种方式就显得艺高人胆大,既然你是私有的不允许外界直接调用,那么我就利用反射机制强行逼你就范:公开其访问权限。如此一来,原本看似安全的堡垒顷刻间沦为炮灰,Singleton再次沦陷。

4. Break Singleton with Classloaders

public static void checkClassloader() throws Exception {
    String className = "fernando.lee.singleton.EagerSingleton";
    ClassLoader classLoader1 = new MyClassloader();
    Class<?> clazz1 = classLoader1.loadClass(className);

    ClassLoader classLoader2 = new MyClassloader();
    Class<?> clazz2 = classLoader2.loadClass(className);

    System.out.println("classLoader1 = " + clazz1.getClassLoader());
    System.out.println("classLoader2 = " + clazz2.getClassLoader());

    Method getInstance1 = clazz1.getDeclaredMethod("getInstance");
    Method getInstance2 = clazz2.getDeclaredMethod("getInstance");
    Object a = getInstance1.invoke(null);
    Object b = getInstance2.invoke(null);

    assertEquals(a, b);
}

Java中一个类并不是单纯依靠其全包类名来标识的,而是全包类名加上加载它的类加载器共同确定的。因此,只要是用不同的类加载器加载的Singleton类并不认为是相同的,因此单例会再次被破坏,通过自定义编写的MyClassLoader即可实现。

由此看来,Singleton唯有妥善关闭了如上所述的诸多后门才能称得上真正的单例。笔者了解到通常有两种应对措施:现有基础上堵住所有漏洞和摈弃旧貌采取创新。简而言之,第一种方式通过完善现有实现让克隆、序列化、反射和类加载器无从下手,第二种方式则采取枚举类型间接实现单例。

1. Safe Singleton

public class SafeSingleton implements Serializable, Cloneable {

    private static final long serialVersionUID = -4147288492005226212L;

    private static SafeSingleton INSTANCE = new SafeSingleton();

    private SafeSingleton() {
        if (INSTANCE != null) {
            throw new IllegalStateException("Singleton instance Already created.");
        }
    }

    public static SafeSingleton getInstance() {
        return INSTANCE;
    }

    private Object readResolve() throws ObjectStreamException {
        return INSTANCE;
    }

    public Object clone() throws CloneNotSupportedException {
        throw new CloneNotSupportedException("Singleton can't be cloned");
    }
}

在原有Singleton的基础上完善若干方法即可实现一个安全的更为纯正的Singleton。注意到当实例已经存在时试图通过调用私有构造函数会直接报错从而抵御了反射机制的入侵; 让调用clone方法直接报错避免了实例被克隆;覆写readReslove方法直接返回现有的实例本身可以防止反序列化过程中生成新的实例。而对于不同类加载器导致的单例模式破坏笔者暂 未亲测出切实可行的应对方案,还烦请大牛提供高见。

2. Enum Singleton

public enum EnumSingleton{
    INSTANCE;

    private EnumSingleton(){
    }
}

采用枚举的方式实现Singleton非常简易,而且可直接通过EnumSingleton.INSTANCE获取该实例。Java中所有定义为enum的类内部都继承了Enum类,而Enum具备的特性包括类加载是静态的来保证线程安全,而且其中的clone方法是final的且直接抛出CloneNotSupportedException异常因而不允许拷贝,同时与生俱来的序列化机制也是直接由JVM掌控的并不会创建出新的实例,此外Enum不能被显式实例化反射破坏也不起作用。当然它也不是没有缺点,比如由于已经隐式继承Enum所以无法再继承其他类了(Java的单继承模式限制),而且相信大多数人并不乐意仅仅为了实现一个纯正的Singleton就将更为习惯的class修改为enum。

  本文介绍了基础篇和进阶篇的Singleton,看似浅显易懂的单例模式没想到也涵盖了那么多学问,希望大家看了有所收获,如果有兴趣亲自实践一番相信更是大有裨益。

  

References:
http://www.tuicool.com/articles/uuuy2m
http://www.javacodegeeks.com/2014/05/java-singleton-design-pattern.html
http://javarevisited.blogspot.com/2011/03/10-interview-questions-on-singleton.html
http://javarevisited.blogspot.com/2012/07/why-enum-singleton-are-better-in-java.html
http://blog.csdn.net/fg2006/article/details/6409423

  


为尊重原创成果,如需转载烦请注明本文出处:http://www.cnblogs.com/fernandolee24/p/5366720.html,特此感谢

  

作者:Aπoλλων
来源链接:https://www.cnblogs.com/fernandolee24/p/5366720.html

版权声明:
1、JavaClub(https://www.javaclub.cn)以学习交流为目的,由作者投稿、网友推荐和小编整理收藏优秀的IT技术及相关内容,包括但不限于文字、图片、音频、视频、软件、程序等,其均来自互联网,本站不享有版权,版权归原作者所有。

2、本站提供的内容仅用于个人学习、研究或欣赏,以及其他非商业性或非盈利性用途,但同时应遵守著作权法及其他相关法律的规定,不得侵犯相关权利人及本网站的合法权利。
3、本网站内容原作者如不愿意在本网站刊登内容,请及时通知本站(javaclubcn@163.com),我们将第一时间核实后及时予以删除。


本文链接:https://www.javaclub.cn/java/17599.html

分享给朋友:

“最适合作为Java基础面试题之Singleton模式” 的相关文章

Spring Cloud面试问题

Spring Cloud面试问题

问:什么是Spring Cloud?     答: Spring Cloud Stream App Starters是基于Spring Boot的Spring Integration应用程序,提供与外部系统的集成。Spring Cloud Task。...

MySQL面试有这一篇就够了

MySQL面试有这一篇就够了

MySQL面试常见知识点 1、 MySQL常用的存储引擎有什么?它们有什么区别? InnoDB InnoDB是MySQL的默认存储引擎,支持事务、行锁和外键等操作。 MyISAM MyISAM是M...

什么是软件实施?软件实施前景几何?软件实施的面试题有那些?

事情是这样的,由于自己目前还没有对象,就想着在兰州找一份还不错的工作,于是投了一家在我的家乡还算不错的公司,对方却说有可能是软件实施岗位,于是趁机了解了一下, 什么是软件实施? 软件实施掌握的基础知识有哪些? 软件实施前景几何?...

分布式|为什么面试官都喜欢问redis的布隆过滤器实现原理?

三、布隆过滤器实现原理 可以把布隆过滤器理解为一个不怎么精确的set结构,当你使用它的contains方法判断某个对象是否存在时,他可能会误判,但是布隆过滤器也不是特别不精确,只要参数设置的合理,它的精确度也是可以得到控制的,只会有小小的...

并发编程|说完AQS,面试官为何不淡定了?

并发编程|说完AQS,面试官为何不淡定了?

你能说下什么是AQS AQS是队列同步器AbstractQueueSynchronizer的简写,它是用来构建锁和其他同步组件的基础框架,它定义了一个全局的int 型的state变量,通过内置的FIFO(先进先出)队列来完成资源竞...

备战BAT面试

备战BAT面试

关注公众号“AI码师”领取2021最新JAVA面试资料一份 为什么说是两千万呢,为什么不说100万,200万呢? 这个当然不是乱说的,是通过计算得来的,我接下来会在文章里面告诉大家这个数据是如何计算的。 在计...

IOS面试题详解(二)..

IOS面试题详解(二)..

上一篇文章列出了共32道IOS面试题: http://www.cnblogs.com/fkdd/archive/2012/03/13/2394724.html 下面从第一题开始解答: 题目:1.Object-c的类可以多重继承么?可以实现多个接口么?Category是什么?...

面试官问:为什么你们项目要用消息队列?

面试官问:为什么你们项目要用消息队列?

同学们应该都会被问到过这个问题:你的系统为什么要用消息队列? 大家普遍回答:我入职前,系统里面就已经用了消息队列啊,然后就用了。 其实面试官就是想看看你有没有深入了解过消息队列,有没有认真思考过消息队列解决了哪些问题? ​ 这篇文章主要带大家解决以...

一文高效图解二叉树面试题

一文高效图解二叉树面试题

点击蓝色“码出高效面试的程序媛”关注我, 了解更多技术流行面试题 二叉树,搜索二叉树,是算法面试的必面题。聊聊面试点: 一、树 & 二叉树 树的组成为节点和边,节点用来储存元素。节点组成为根节点、父节点和子节点。 如图:树深 leng...

看完这篇Exception 和 Error,和面试官扯皮就没问题了

看完这篇Exception 和 Error,和面试官扯皮就没问题了

在 Java 中的基本理念是 结构不佳的代码不能运行,发现错误的理想时期是在编译期间,因为你不用运行程序,只是凭借着对 Java 基本理念的理解就能发现问题。但是编译期并不能找出所有的问题,有一些 NullPointerException 和 ClassNotFoundExceptio...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。