`

Java基础面试题之Singleton模式

 
阅读更多

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

 

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

Singleton类图

那接下来我们就看看实现一个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

分享到:
评论

相关推荐

    最全java面试题及答案(208道).pdf

    本文总结了Java面试题及答案,涵盖了Java基础、容器、多线程、反射、对象拷贝、JavaWeb、异常、网络、设计模式、Spring/Spring MVC、Spring Boot/SpringCloud、Hibernate、MyBatis、RabbitMQ、Kafka、Zookeeper、...

    java程序员面试题

    Java程序员面试题是一个涵盖广泛领域的主题,涉及到Java基础、Web开发、设计模式、数据库和框架等多个方面。以下是对这些文件内容的详细解析: 1. **Model1-CoreJava.doc & Model1-CoreJava-answer.doc**: 这部分...

    2016 Java 基础 面试题

    ### 2016 Java 基础面试题解析 #### 1. **throw 和 throws 的区别** 在Java中,`throw`关键字用于手动抛出一个异常,而`throws`...以上就是2016年Java基础面试题的相关知识点解析,希望对准备面试的朋友们有所帮助。

    JAVA工程师面试题和一些经典题

    在Java工程师的面试中,单例模式和多线程是两个非常重要的知识点,它们既是基础,也是进阶技能,往往能体现一个开发者的编程素养和技术深度。以下将详细阐述这两个主题。 **单例模式** 单例模式是一种设计模式,它...

    Java面试题大汇总(2021年Java面试题大全带答案)

    以下是Java面试题大汇总,涵盖了Java语言的多个方面,包括Java基础知识、Java高级面试题、Java工程师面试题等。 Java基础知识 * Java语言基础:Java是一种基于对象的编程语言,具有平台独立性、简单性、面向对象等...

    深圳各公司java面试题

    #### Java基础知识面试题解析 **1. 实现序列化的方法?** 序列化是将对象转换为字节流的过程,以便在网络上传输或永久存储。在Java中,实现序列化需要让类实现`Serializable`接口,然后使用`ObjectOutputStream`和`...

    java程序员经典面试题

    本资源为Java程序员面试必备资料,涵盖了Java基础、线程编程、JSP、Servlet、JDBC、JDO、EJB、应用服务器、J2EE、MVC、设计模式等多个方面的知识点,旨在帮助Java程序员更好地准备面试,获得成功。 Java基础 Java...

    java面试题以及技巧

    │ │ │ ├─培训主讲之题 │ │ │ │ Desktop_.ini │ │ │ │ question.rar │ │ │ │ │ │ │ └─培训教程 │ │ │ Desktop_.ini │ │ │ SL275_OH_GB.pdf │ │ │ │ │ ├─考前预测的三套题 │ │...

    JAVA基础面试,SpringCloud面试题

    综上所述,Java基础面试题涉及了语言特性和设计模式等多个方面,而Spring框架的相关知识点则重点关注于事务管理和配置等方面。这些内容对于Java开发者而言是非常重要的基础知识,掌握它们能够帮助开发者更好地理解和...

    Java程序员面试题集

    Java程序员面试时,通常会遇到一系列关于语言特性和框架的问题,这些题目旨在考察候选人的基础知识、问题解决能力和实践经验。以下是对这些面试题目的详细解答: 1. **final, finally, finalize的区别**: - `...

    java面试题大全(2012版)

    Java基础部分 7 1、一个".java"源文件中是否可以包括多个类(不是内部类)?有什么限制? 7 2、Java有没有goto? 7 3、说说&和&&的区别。 8 4、在JAVA中如何跳出当前的多重嵌套循环? 8 5、switch语句能否作用在byte...

    2024年java面试题

    Java面试题涵盖了许多核心概念,包括基础语法、JVM、线程、Spring框架以及MySQL数据库的相关知识。以下是对这些知识点的详细解释: 1. **接口与抽象类的区别**: - 接口是完全抽象的,只能包含方法签名,不能有...

    java面试题及技巧4

    │ │ │ ├─培训主讲之题 │ │ │ │ Desktop_.ini │ │ │ │ question.rar │ │ │ │ │ │ │ └─培训教程 │ │ │ Desktop_.ini │ │ │ SL275_OH_GB.pdf │ │ │ │ │ ├─考前预测的三套题 │ │...

    Java se 面试题.docx

    Java SE面试题主要涵盖了许多核心概念,其中包括变量自增、...总结,Java SE面试题涵盖了基础语法和高级设计模式。理解自增运算符的行为和单例模式的实现是Java开发者必备的知识,它们在实际编程中有着广泛的应用。

    java面试题.doc

    本文档汇集了 Java 相关的面试题,涵盖了 Java 基础知识、多线程编程、数据库操作、XML 解析、EJB 等方面。 1. Singleton 模式 Singleton 模式主要是为了保证在 Java 应用程序中,一个类 Class 只有一个实例存在...

    java经典面试32题

    ### Java经典面试题详解 #### 1. final, finally, finalize 的区别 - **final**:此关键字在Java中主要用于修饰变量、方法或类。当用于变量时,表示该变量一旦被赋值就不能再更改(对于引用类型的变量,指的是引用...

    java经典面试题3

    ### Java经典面试题详解 #### 1. 抽象类与接口的区别,是否可以同时实现多个接口?是否抽象类可以实现具体方法? 在Java中,**抽象类**和**接口**是面向对象编程中用于定义行为规范的重要工具,但它们之间存在显著...

    十套c#面试题,基本包含了所有c#知识

    在面试题中,我们可以看到多种设计模式的概念,如 Singleton、抽象工厂、Bridge、Composite 等。Singleton 模式是一种创建型模式,用于创建一个独特的对象。抽象工厂模式是一种创建型模式,用于创建一组相关的对象。...

    企业java相关面试题,包含前端面试资料.zip

    一、Java基础知识面试题 Java是企业级开发的基石,其面试题主要涉及以下几个方面: 1. Java语法:包括变量、数据类型、运算符、流程控制语句(如if、switch、for、while)、异常处理、面向对象特性(封装、继承、...

Global site tag (gtag.js) - Google Analytics