GOF23设计模式之单例模式-Singleton

本文将介绍单例模式的8种不同实现,由浅至深,详细剖析单例模式的每种实现优缺点,希望能给各位同学看完本文对单例模式有一个更加全面的认识与了解。

概述

  • 定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
  • 类型:创建型设计模式
  • 适用场景:想确保任何情况下都绝对只有一个实例
  • 优点
    1. 在内存只有一个实例,减少了内存开销。
    2. 可以避免对资源的多重占用
    3. 设置全局访问点,严格控制访问
  • 缺点:没有接口,扩展困难。
  • 重点概念
    • 私有构造器
    • 线程安全
    • 双重检查锁机制
    • 延迟加载
    • 序列化和反序列化安全
    • 防反射攻击

单例实现

1.懒汉式(线程不安全)

  • 描述
    懒汉式,顾名思义,就是比较“懒”,实例需要等调用时才会创建。

  • coding

/**
 * @Author yong.zheng
 * @Date 2021/12/16、18:43
 */
public class LazySingletonThreadNotSafe {
    // 1、重点:私有构造器
    private LazySingletonThreadNotSafe() {}
    // 2、静态变量
    private static LazySingletonThreadNotSafe lazySingleton = null;

    public static LazySingletonThreadNotSafe getLazySingleton() {
        if (lazySingleton == null) {
            lazySingleton = new LazySingletonThreadNotSafe();
        }
        return lazySingleton;
    }
}

  • 分析
    这种方式是最基本的实现方式,这种实现最大的问题就是不支持多线程。在多线程的情况下,可能获取的对象实例并不是单例的,所以严格意义上它并不算单例模式。

2.懒汉式(基于方法锁的线程安全)

  • 描述
    综上,我们可以通过加锁synchronized/Lock的方式,保证线程安全.
  • coding
/**
 * @Author yong.zheng
 * @Date 2021/12/16、18:56
 * 2、懒汉式。synchronized 方法加锁保证线程安全
 */
public class LazySingletonThreadSafe {

    private LazySingletonThreadSafe() {
    }

    private static LazySingletonThreadSafe lazySingleton = null;

    // 方法加锁保证线程安全,
    // 缺点:静态方法加锁,等于锁整个类,其影响整个类的性能
    public synchronized static LazySingletonThreadSafe getLazySingleton() {
        if (lazySingleton == null) {
            lazySingleton = new LazySingletonThreadSafe();
        }
        return lazySingleton;
    }
}

  • 分析

    基于实现1的基础上,在获取单例对象的方法上加锁,保证线程安全;

    但由于Synchronize的所在静态方法上,相当于锁住了整个类,这样是非常影响性能的,如何保证线程安全的情况下,又要兼顾自性能呢?

2.懒汉式(Double Check)

  • 描述
    如何保证线程安全的情况下,又要兼顾自性能呢?
  • coding
/**
 * @Author yong.zheng
 * @Date 2021/12/16、18:58
 * 3、 懒汉式、double check保证线程安全且兼顾性能,volatile防止指令重排
 */
public class LazySingletonDoubleCheck {
    private LazySingletonDoubleCheck() {
    }

    private volatile static LazySingletonDoubleCheck lazySingletonDoubleCheck = null;

    public static LazySingletonDoubleCheck getLazySingletonDoubleCheck() {
        if (lazySingletonDoubleCheck == null) {
            synchronized (LazySingletonDoubleCheck.class) {
                if (lazySingletonDoubleCheck == null) {
                    lazySingletonDoubleCheck = new LazySingletonDoubleCheck();
                    // 指令重排
                    //1.分配内存给这个对象
                    //3.设置lazySingletonDoubleCheck 指向刚分配的内存地址
                    //2.初始化对象
                }
            }
        }
        return lazySingletonDoubleCheck;
    }
}

  • 分析
    • Double Check:
      这种方式采用双锁机制,安全且在多线程情况下能保持高性能。

    • 指令重排的问题分析:
      下面 这段代码:

      lazySingletonDoubleCheck = new LazySingletonDoubleCheck();

      看似一行代码,实则是执行了好多步骤,如下:

      //1.分配内存给这个对象
      //2.初始化对象
      //3.设置lazySingletonDoubleCheck 指向刚分配的内存地址

      Java的设计时为了程序的性能,也是允许指令重排序的。在单线程的情况下不影响程序执行结果时,计算机可能会进行指令重排,即步骤2和3可能重排颠倒,达到更快的执行效率。

      在多线程的情况下,指令发出重排时,将会出现下图问题。若线程0构造对象时,指令发生重排,先将instance指向了分配的空内存,而线程0还未来得及初始化对象,或者对象还未刷入主存,此时线程1进入判断,发现instance不是已经分配内存空间,认为对象已创建,直接返回了instance,此时线程1拿到instance可能是一个未初始化完成的对象,这样就会有问题

      在这里插入图片描述
      如何解决上述指令重排导致的问题呢?

      在静态变量中加入volatile 关键字,防止new Instance时发生指令重排序,并且将线程0操作结果能立即刷新至主存,保证数据的可见性

      volatile关键字作用:
      1. 禁止重排序
      2. 保证数据可见性

    • 拓展:
      Java并发编程:volatile关键字解析”,笔者认为这边文章讲得非常好。
      在这里插入图片描述

4.懒汉式(基于静态内部类的延迟加载)

  • 描述

    这种方式能达到双检锁方式一样的功效,且实现更简单。

    对静态域使用延迟初始化,应使用这种方式而不是双检锁方式。这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。

  • coding

/**
 * @Author yong.zheng
 * @Date 2021/12/16、19:06
 * 4、基于静态内部类的延迟加载
 */
public class StaticInnerSingleton {
    private StaticInnerSingleton(){
    }

    private static class InnerClass {
        private static final StaticInnerSingleton INSTANCE = new StaticInnerSingleton();
    }

    public static final StaticInnerSingleton getInstance() {
        return InnerClass.INSTANCE;
    }

}

  • 分析
    这种方式同样利用了 classloader 机制来保证初始化 instance 时只有一个线程,它跟第 5 种方式不同的是:第 5 种方式只要 Singleton 类被装载了,那么 instance 就会被实例化(没有达到 lazy loading 效果),而这种方式是 Singleton 类被装载了,instance 不一定被初始化。因为 InnerClass类没有被主动使用,只有通过显式调用 getInstance 方法时,才会显式装载 InnerClass类,从而实例化 instance。想象一下,如果实例化 instance 很消耗资源,所以想让它延迟加载,另外一方面,又不希望在 Singleton 类加载时就实例化,因为不能确保 Singleton 类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化 instance 显然是不合适的。这个时候,这种方式相比第5 种方式就显得很合理。

5.饿汉式

  • 描述

    顾名思义,饿汉式在类的初始化阶段时就创建对象。这种方式比较常用,但容易产生垃圾对象。

  • 优点:没有加锁,执行效率会提高。

  • 缺点:类加载时就初始化,浪费内存。

  • coding

/**
 * @Author yong.zheng
 * @Date 2021/12/16、19:10
 * 5、饿汉式(1)
 */
public class HungrySingleton {
    private HungrySingleton(){}

    private static final HungrySingleton hungrySingleton = new HungrySingleton();

    public static HungrySingleton getHungrySingleton(){
        return hungrySingleton;
    }
}

/**
 * @Author yong.zheng
 * @Date 2021/12/16、19:13
 * 5、饿汉式(2)
 */
public class HungrySingleton2 {
    private HungrySingleton2() {
    }
    private final static HungrySingleton2 staticSingleton;
    static {
        staticSingleton = new HungrySingleton2();
    }

    public static HungrySingleton2 getStaticSingleton(){
        return staticSingleton;
    }
}
  • 分析
    它基于 classloader 机制避免了多线程的同步问题,不过,instance 在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用 getInstance 方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化 instance 显然没有达到 lazy loading 的效果。

6.枚举单例(Java中单例模式的最佳实践!)

  • 描述

    在Java中,基于枚举的特性实现是单例模式的最佳实践。why?详见下面章节中单例模式中序列化与反序列化问题分析和防止放射攻击。本节直接coding吧。

  • coding

/**
 * @Author yong.zheng
 * @Date 2021/12/16、14:18
 * 7、单例模式的最佳实践: 基于枚举的
 */
public enum EnumSingleton {
    INSTENCE;

    public static EnumSingleton getInstance(){
        return INSTENCE;
    }
}


7.容器单例(单例模式+享元模式)

public class ContainerSingleton {

    private ContainerSingleton(){

    }
    private static Map<String,Object> singletonMap = new HashMap<String,Object>();
    //private static Map<String,Object> singletonMap = new Hashtable<String, Object>();
    //private static Map<String,Object> singletonMap = new ConcurrentHashMap<String, Object>();
    
    public static void putInstance(String key,Object instance){
        if(StringUtils.isNotBlank(key) && instance != null){
            if(!singletonMap.containsKey(key)){
                singletonMap.put(key,instance);
            }
        }
    }

    public static Object getInstance(String key){
        return singletonMap.get(key);
    }
}

可以看出这种实现是不安全的,singletonMap.put(key,instance);多线程初始化时可能会出现多个实例,也可以通过HashTable、ConcurrentHashMap去保证线程安全。 Map从入门到性能分析

  • 思维拓展:Spring Ioc容器中单例怎么实现的?

    懒汉式?饿汉式?哈哈…都不是。

    而且与本文的容器单例是没有关系的,不过可以类比一下,emo…,Spring是采用单例注册表的方式进行实现。

    Spring的单例模式底层实现学习笔记

8.线程"单例"(基于ThreadLocal的实现)

  • 描述

    基于ThreadLocal实现每个线程获取的对象实例是单例的。

  • coding

public class ThreadLocalInstance {
    private static final ThreadLocal<ThreadLocalInstance> threadLocalInstanceThreadLocal
             = new ThreadLocal<ThreadLocalInstance>(){
        @Override
        protected ThreadLocalInstance initialValue() {
            return new ThreadLocalInstance();
        }
    };
    private ThreadLocalInstance(){

    }

    public static ThreadLocalInstance getInstance(){
        return threadLocalInstanceThreadLocal.get();
    }

}

破坏单例

序列化与反序列化破坏单例模式

  • 概述:通过序列化和反序列化再次获取的实例,破坏程序设计的单例模式

  • coding测试

/**
 * @Author yong.zheng
 * @Date 2021/12/16、19:52
 * 序列化与反序列化破坏单例模式
 */
public class SerializeTest {

    public static void main(String[] args) throws Exception {
        HungrySingleton hungrySingleton = HungrySingleton.getHungrySingleton();
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test"));
        oos.writeObject(hungrySingleton); // 序列化
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("test"));
        HungrySingleton oisSingleton = (HungrySingleton) ois.readObject();//反序列化
        System.out.println(hungrySingleton);
        System.out.println(oisSingleton);
        System.out.println(oisSingleton == hungrySingleton);
    }
}

-- 执行结果如下:
cn.org.yongz.design.pattern.creational.singleton.HungrySingleton@14ae5a5
cn.org.yongz.design.pattern.creational.singleton.HungrySingleton@6d03e736
false

我们看上面代码执行结果,当hungrySingleton序列化之后,再被反序列化之后获取的对象与之前的对象并不相等,如此一来就破坏了单例。

如何解决反序列化破坏单例呢?
看下面代码,我们在HungrySingleton 中加入 readResolve()返回单例对象

public class HungrySingleton  implements Serializable {
    private HungrySingleton(){}

    private static final HungrySingleton hungrySingleton = new HungrySingleton();

    public static HungrySingleton getHungrySingleton(){
        return hungrySingleton;
    }
    // 解决序列化反序列化破坏单例。
    private Object readResolve(){
        return hungrySingleton;
    }
}

在看上面测试程序执行结果:

cn.org.yongz.design.pattern.creational.singleton.HungrySingleton@14ae5a5
cn.org.yongz.design.pattern.creational.singleton.HungrySingleton@14ae5a5
true

执行结果就很神奇,序列化破坏单例的问题就解决了,why?加入 readResolve()方法,就能解决反序列化破坏单例?
在这里插入图片描述

下面我们分析一下Java在反序列化时执行的源码。

那我们看看反序列化时调用的readObject方法
在这里插入图片描述
进去看看readObject0()
在这里插入图片描述
在看看readOrdinaryObject(),可以看出反射创建了我们的目标对象实例。

在这里插入图片描述

在这里插入图片描述
通过上面代码我们终于找到了如下几个答案:

	1、为什么序列化、再反序列化会破坏单例呢?
		原因很简单,因为反序列化时,会通过反射创建出新的实例对象,所以这样就会破坏单例啦!
	2、为什么添加如上代码就能解决序列化带来的问题呢?
		原来在反序列化的时候,虽然通过反射创建了新的实例对象,但是会去判断一下类中是否有
		readResolve()方法,有则调用此方法,并将返回值覆盖反射产生的实例对象。而我们写的
		readResolve方法中返回的就是类的单例,所以我们反序列化最终拿到的对象就是单例。
	3、那反序列化过程中实际有没有创建新的实例对象呢?
		通过上面源码的分析,实际在反序列化过程中实际是创建了新的实例对象,只不过最终被
		readResolve返回的单例覆盖了而已。

通过上面编码和分析我们解决了实现6的序列化破坏单例的问题,其他实现请自行解决。

思维拓展:JSON/XML序列化方式是否破坏单例?

  • 描述:
    实际开发过程中,我们经常会用到json和xml等序列化保存实例对象。本文就不一一coding出来了,有兴趣的自己去看看吧。

例如fastjson

    public static void main(String[] args) {
        HungrySingleton instance = HungrySingleton.getHungrySingleton();
        String jsonOutput= JSON.toJSONString(instance);
        HungrySingleton jsonParseInstance = JSON.parseObject(jsonOutput, HungrySingleton.class);
        System.out.println(instance);
        System.out.println(jsonParseInstance);
        System.out.println(jsonParseInstance == instance);
    }

其实,这种基本都是通过反射构造新的实例对象。也会破坏单例的,再看看下面怎么防反射攻击吧。

反射攻击

  • 概述:通过反射暴力打开私有构造器,通过构造器创建新的实例对象,从而破坏单例模式。
  • coding测试
/**
 * @Author yong.zheng
 * @Date 2021/12/16、20:07
 * 反射攻击单例模式
 */
public class ReflectTest {
    public static void main(String[] args) throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
        HungrySingleton instance = HungrySingleton.getHungrySingleton();
        Class<HungrySingleton> clazz = HungrySingleton.class;
        Constructor constructor = clazz.getDeclaredConstructor();
        constructor.setAccessible(true);
        HungrySingleton newInstance = (HungrySingleton) constructor.newInstance();
        System.out.println(instance);
        System.out.println(newInstance);
        System.out.println(newInstance == instance);
    }
}

--- 执行结果
cn.org.yongz.design.pattern.creational.singleton.HungrySingleton@1b6d3586
cn.org.yongz.design.pattern.creational.singleton.HungrySingleton@4554617c
false

很显然,我们通过反射直接调用私有构造器构造新的实例对象,也达到了破坏单例模式的效果。

如何解决?

  1. 通过限制反射调用构造器
public class HungrySingleton  implements Serializable {
    private HungrySingleton(){
        if(hungrySingleton != null){
            throw new RuntimeException("单例构造器禁止反射调用");
        }
    }

    private static final HungrySingleton hungrySingleton = new HungrySingleton();

    public static HungrySingleton getHungrySingleton(){
        return hungrySingleton;
    }
    // 解决序列化反序列化破坏单例。
    private Object readResolve(){
        return hungrySingleton;
    }
}

这样通过反射调用私有构造器时就会报错。

Exception in thread "main" java.lang.ExceptionInInitializerError
	at cn.org.yongz.design.pattern.creational.singleton.ReflectTest.main(ReflectTest.java:13)
Caused by: java.lang.RuntimeException: 禁止反射调用私有构造器
	at cn.org.yongz.design.pattern.creational.singleton.HungrySingleton.<init>(HungrySingleton.java:12)
	at cn.org.yongz.design.pattern.creational.singleton.HungrySingleton.<clinit>(HungrySingleton.java:15)
	... 1 more

  • 问题
    那么之前实现的懒汉式能否防止这样放射攻击呢?其实是不可以的,其实上面方案只能防止饿汉式和静态内部类的实现方式,并且变量要用final修饰。因为在懒汉式中,如果先通过反射构造对象,将会无限构造新的对象,原因是单例的成员变量还未被构造,成员变量判断==null会一直成立。能不能写一下控制条件去判断,当然是不能的,不论怎么判断,反射都能将成员变量进行修改,而final修改的是不能别修改的,并且饿汉式和静态内部类的方式都是在类加载的时候就会生成单例对象。

拓展问题:上述防反射攻击处理后会不会影响到反序列化生成对象?

  • 描述:
    上述代码中,我们将私有构造器进行了异常处理,防止放射调用,而之前反序列化中提到,反序列化时会通过反射构造新的对象实例,如果类中存在readResolve()方法,新的对象实例就会调用readResolve方法覆盖反射生成的实例对象。既然是将构造器进行处理了,那反序列化过程中会不会也会抛出异常?

  • coding

    在之前的饿汉式私有构造器中打印一个日志。把readResolve先注释(其实不注释也不影响测试,原因是:不论有没有此方法,反序列化时都会产生新的实例对象)

    public class HungrySingleton  implements Serializable {
        private HungrySingleton(){
            System.out.println("调用了构造器!");
            if(hungrySingleton != null){
                throw new RuntimeException("单例构造器禁止反射调用");
            }
        }
     
        private static final HungrySingleton hungrySingleton = new HungrySingleton(null);
    
        public static HungrySingleton getHungrySingleton(){
            return hungrySingleton;
        }
        // 解决序列化反序列化破坏单例。
    //    private Object readResolve(){
    //        return hungrySingleton;
    //    }
        
    }
    

    测试类

    public class SerializeTest {
    //    private static final ReflectionFactory reflFactory =
    //            AccessController.doPrivileged(
    //                    new ReflectionFactory.GetReflectionFactoryAction());
        public static void main(String[] args) throws Exception {
            HungrySingleton hungrySingleton = HungrySingleton.getHungrySingleton();
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test"));
            oos.writeObject(hungrySingleton); // 序列化
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream("test"));
            HungrySingleton oisSingleton = (HungrySingleton) ois.readObject();//反序列化
            System.out.println(hungrySingleton);
            System.out.println(oisSingleton);
            System.out.println(oisSingleton == hungrySingleton);
    
    //        提取了反序列时构造新的实例对象的源码
    //        Class<?> cl =  HungrySingleton.class;
    //        Class initCl = cl.getSuperclass();
    //        Constructor<?> cons = initCl.getDeclaredConstructor((Class<?>[]) null);
    //        cons = reflFactory.newConstructorForSerialization(cl, cons);
    //        HungrySingleton hungrySingleton3 = (HungrySingleton) cons.newInstance(null);
    //        System.out.println(hungrySingleton3);
            
        }
    }
    
    

    先不看注释的代码,看执行结果:

    调用了构造器!
    cn.org.yongz.design.pattern.creational.singleton.HungrySingleton@14ae5a5
    cn.org.yongz.design.pattern.creational.singleton.HungrySingleton@6d03e736
    false
    

    分析执行结果:

    • 问题1:反序列化成功的生成了新的实例对象,why?
      再看执行结果,我们类中定义的构造器就被调用了一次!啊?。。。原来新的对象实例不是通过调用我们的构造器生成的。

    • 问题2:反序列化是如何通过反射构造的新的对象实例呢?
      出于好奇心,我又再次看了遍源码,并把构造实例的核心的代码抽取出来了,也就是上面测试类中注释掉的代码。下面一起分析源码吧!

    • 问题3:cons是个啥?
      之前看到,desc.newInstance()构造的新的实例对象
      在这里插入图片描述
      如何构建的呢?
      在这里插入图片描述
      原来是通过这个成员变量cons(构造器)构造的,cons是什么时候赋值的,它又是个什么构造器呢?在代码中搜索就发现了两个地方给cons赋值,看下面
      在这里插入图片描述
      我们的类似实现Serializable的走下面,进去看看。
      在这里插入图片描述
      判断首先是否有基类,然后拿到基类的Class对象,并获取基类的构造器,然后调用了第4个框框里面的代码。
      之前上面测试类中,就是抽取了这里的代码,好奇的你赶紧去试试吧。

      那ReflectionFactory.newConstructorForSerialization()进行了什么操作呢?参考下面几篇文章!学习下反射和序列化吧。
      reflectionFactory.newConstructorForSerialization原理
      避开类本身的构造函数

  • 补充:

    • 有人质疑说reflectionFactory.newConstructorForSerialization,必须要实现Serializable接口的类,才能通过此方法构建,我测试了一下,将HungrySingleton类不实现Serializable接口,测试发现,仍然能生成新的实例对象
  • 综上所述
    无论上面怎么折腾,我们通过反射还是总有办法破坏我们系统中的单例(可哪个开发人员这么无聊自己写单例,然后又用反射破坏单例啊!emo…)。
    在这里插入图片描述

    我们看看下面如何非常优雅的实现单例模式并且能够规避反射攻击和反序列化问题吧!

枚举单例为什么是最佳实践

上面章节中,我们就提到了枚举单例是单例模式的最佳是实践,枚举的特性天然性的解决了反序列化和反射攻击对单例的破坏。

  • Why
    我们将之前的枚举单例的实现,通过Jad反编译,查看下反编译源码:
public final class EnumSingleton extends Enum
{

    public static EnumSingleton[] values()
    {
        return (EnumSingleton[])$VALUES.clone();
    }

    public static EnumSingleton valueOf(String name)
    {
        return (EnumSingleton)Enum.valueOf(cn/org/yongz/design/pattern/creational/singleton/EnumSingleton, name);
    }

    // 私有构造器
    private EnumSingleton(String s, int i)
    {
        super(s, i);
    }

    // 获取单例对象
    public static EnumSingleton getInstance()
    {
        return INSTENCE;
    }

    // 静态常量
    public static final EnumSingleton INSTENCE;
    private static final EnumSingleton $VALUES[];

    // 3、静态代码块,初始化实例
    static 
    {
        INSTENCE = new EnumSingleton("INSTENCE", 0);
        $VALUES = (new EnumSingleton[] {
            INSTENCE
        });
    }
}


通过反编译的源码,很明显就能看出,其实源码与我们之前实现的饿汉式(2)一模一样。

  • 序列化反序列化测试
    public static void main(String[] args) throws Exception {
        EnumSingleton instance = EnumSingleton.getInstance();
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test2"));
        oos.writeObject(instance); // 序列化
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("test2"));
        EnumSingleton newInstance = (EnumSingleton) ois.readObject();//反序列化
        System.out.println(instance);
        System.out.println(newInstance);
        System.out.println(newInstance == instance);
    }
INSTENCE
INSTENCE
true

测试发现,enum确实能防止反序列化破坏单例,我们再看下反序列化的代码:
在这里插入图片描述
进去再看,对比之前Object通过反射构造新的实例,由于枚举的特性,这里通过枚举名称获取的枚举类是全局唯一的。
在这里插入图片描述

  • 反射攻击测试

/**
 * @Author yong.zheng
 * @Date 2021/12/16、19:26
 */
public class EnumReflectTest {
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        EnumSingleton instance = EnumSingleton.getInstance();
        Class<EnumSingleton> clazz = EnumSingleton.class;
        Constructor constructor = clazz.getDeclaredConstructor(String.class,int.class);
        constructor.setAccessible(true);
        EnumSingleton newInstance = (EnumSingleton) constructor.newInstance();
        System.out.println(instance);
        System.out.println(newInstance);
        System.out.println(newInstance == instance);
    }
}
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
	at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
	at cn.org.yongz.design.pattern.creational.singleton.EnumReflectTest.main(EnumReflectTest.java:16)

枚举是不能通过反射创建的。

        Class<?> cl = EnumSingleton.class;
        Class initCl = cl.getSuperclass();
        Constructor<?> cons = initCl.getDeclaredConstructor((Class<?>[]) null);
 

第三行将会报错, Exception in thread “main” java.lang.NoSuchMethodException: java.lang.Enum.()
如果在获取其他父类 initCl.getSuperclass()的构造器,最终会发现执行结果获取的对象是个null。

Enum,强! 不仅能够优雅的实现单例,还能规避上述操作而破坏单例!

原型模式破坏单例模式

  • 描述:即通过clone方法破坏单例
  • coding:

默认实现Cloneable接口。

public class HungrySingleton  implements Serializable,Cloneable {
    private HungrySingleton(){
        System.out.println("调用了构造器!");
        if(hungrySingleton != null){
            throw new RuntimeException("单例构造器禁止反射调用");
        }
    }

    private static final HungrySingleton hungrySingleton = new HungrySingleton();

    public static HungrySingleton getHungrySingleton(){
        return hungrySingleton;
    }
    // 解决序列化反序列化破坏单例。
    private Object readResolve(){
        return hungrySingleton;
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

测试类

    public static void main(String[] args) throws CloneNotSupportedException {
        HungrySingleton instance = HungrySingleton.getHungrySingleton();
        HungrySingleton cloneInstance = (HungrySingleton) instance.clone();
        System.out.println(instance);
        System.out.println(cloneInstance);
        System.out.println(cloneInstance == instance);
    }

执行结果:

调用了构造器!
cn.org.yongz.design.pattern.creational.singleton.HungrySingleton@1b6d3586
cn.org.yongz.design.pattern.creational.singleton.HungrySingleton@4554617c
false

分析:通过clone方法也能构造新的对象实例,从而破坏单例。解决办法重写clone方法返回单例对象即可。
解决办法:

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return hungrySingleton;
    }

后面会在原型模式中深入讲解clone方法、深拷贝、浅拷贝等相关问题。

框架中的Singleton

后续补…

总结

疑难杂症

本文解决了哪些问题?

  1. 单例的几种实现,有何异同?
  2. 什么是序列化与反序列化?如何防止反序列化破坏单例?
  3. 何为反射破坏单例?如何避免?
  4. 如何优雅的实现单例模式?
  5. 框架中如何运用单例模式的?

小结

看似单例模式是GOF23种最简单的,实则也是最复杂的!
终于明白为啥单例模式是高频面试题!
在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值