关于单例设计模式,《Java与设计模式之单例模式(上)六种实现方式》介绍了6种不同的单例模式,线程安全,本文介绍该如何保证单例模式最核心的作用——“实现该模式的类有且只有一个实例对象”。
      我们知道,Java中有四种方式创建对象:new、克隆、序列化、反射。下面逐一分析哪个方式存在风险。
  • new,无风险。由于单例模式提供的构造函数都是私有的,所以不能在外部使用new的方式创建对象。
  • 克隆,无风险。因为对象必须实现一个Cloneable 接口才可以直接调用clone(),而单例模式并未实现Cloneable 接口,所以,无风险。
  • 序列化机制,有风险。对象序列化成一个字节流后,若要被反序列化恢复时,会生成一个新的对象,此对象和原来的对象具有一模一样的行为,但归根结底是两个对象。
  • 反射机制,有风险。有句老话“反射可以打破一切封装!”,说明了任何类在反射机制面前都是透明的,通过反射机制可以获得类的各种属性,当然也可以获得类的构造函数(包括私有的),从而构造一个新的对象。但是,枚举类除外,下文会提到。
    通过上述分析,若要实现一个完美的单例模式必须考虑序列化和反射问题。本文就序列化机制和反射是如何破坏单例模式,以及枚举类型是如何完美解决这个问题加以解析讨论。
 

通过反射破坏单例

 
      原理很简单,通过反射获取其构造方法,然后重新生成一个实例。

  1. private static void reflectMethod() {
  2. try {
  3. EagerSingleton instance1 = EagerSingleton.getInstance();
  4. // 通过反射得到其构造方法,修改其构造方法的访问权限,并用这个构造方法构造一个对象
  5. Constructor constructor = EagerSingleton.class.getDeclaredConstructor();
  6. // 把私有访问域的访问级别设置为public,否则,会抛异常
  7. constructor.setAccessible(true);
  8. EagerSingleton instance2 = (EagerSingleton) constructor.newInstance();
  9.  
  10. System.out.println( "反射是否破坏了单例 : " + !(instance1 == instance2)); // true
  11. } catch (Exception e) {
  12. e.printStackTrace();
  13. }
  14. }

显然,说好的单例已经变成了多例。防止反射破坏单例可以使用枚举式单例模式。

  1. private static void reflectEnumMethod() {
  2. try {
  3. EnumSingleton instance1 = EnumSingleton.INSTANCE;
  4. Constructor constructor = EnumSingleton.class.getDeclaredConstructor();
  5. constructor.setAccessible(true);
  6. EnumSingleton instance2 = (EnumSingleton) constructor.newInstance();
  7. System.out.println( "反射是否破坏了单例 : " + !(instance1 == instance2)); // true
  8. } catch (Exception e) {
  9. e.printStackTrace();
  10. }
  11. }

执行改方法时抛出如下异常:

  1. java.lang.NoSuchMethodException: com.east7.singleton.EnumSingleton.<init>()
  2. at java.lang.Class.getConstructor0(Class.java:3082)
  3. at java.lang.Class.getDeclaredConstructor(Class.java:2178)
  4. at com.east7.controller.SingletonController.reflectEnumMethod(SingletonController.java:42)
  5. at com.east7.controller.SingletonController.main(SingletonController.java:22)

不言而喻,我们在代码中无法通过反射获取 enum 类的构造方法。对于枚举,JVM 会自动进行实例的创建,其构造方法由 JVM 在创建实例的时候进行调用。

通过序列化机制破坏

     
       下面,我们再说说另一种破解方法:序列化、反序列化。我们知道,序列化是将 java 对象转换为字节流,反序列化是从字节流转换为 java 对象。下面以饿汉式单例为例,验证反序列化会生成新的单例实例。
  1. private static void serialMethod() {
  2. try {
  3. EagerSingleton instance1 = EagerSingleton.getInstance();
  4.  
  5. // instance3 将从 instance1 序列化后,反序列化而来
  6. EagerSingleton instance3 = null;
  7. ByteArrayOutputStream bout = null;
  8. ObjectOutputStream out = null;
  9. try {
  10. bout = new ByteArrayOutputStream();
  11. out = new ObjectOutputStream(bout);
  12. out.writeObject(instance1);
  13.  
  14. ByteArrayInputStream bin = new ByteArrayInputStream(bout.toByteArray());
  15. ObjectInputStream in = new ObjectInputStream(bin);
  16. instance3 = (EagerSingleton) in.readObject();
  17. } catch (Exception e) {
  18. System.out.println(" -------------- " + e);
  19. } finally {
  20. // close bout&out
  21. }
  22. System.out.println( "序列化是否破坏了单例 : " + (instance1 == instance3));
  23. } catch (Exception e) {
  24. e.printStackTrace();
  25. }
  26. }

执行结果打印为false,说明生成了不同的实例。序列化饿汉式单例:

  1. import java.io.ObjectStreamException;
  2. import java.io.Serializable;
  3.  
  4. /**
  5. * 序列化的饿汉式单例,线程安全
  6. */
  7. public class EagerSingletonPlus implements Serializable {
  8.  
  9. private static final EagerSingletonPlus instance = new EagerSingletonPlus();
  10. // 私有化构造方法
  11.  
  12. private EagerSingletonPlus() {
  13. }
  14. public static EagerSingletonPlus getInstance() {
  15. return instance;
  16. }
  17. /**
  18. * 看这里,新增
  19. */
  20. public Object readResolve() throws ObjectStreamException {
  21. return instance;
  22. }
  23.  
  24. // 序列化,新增
  25. private static final long serialVersionUID = -3006063981632376005L;
  26. }

把serialEnumMethod中的EagerSingleton类替换成EagerSingletonPlus,再次执行,结果变成了false,说明反序列化生成的实例instance3和instance1对应同一个实例。因为在反序列化的时候,JVM 会自动调用 readResolve() 这个方法,我们可以在这个方法中替换掉从流中反序列化回来的对象。关于readResolve()的更详细信息,请参考3.7 The readResolve Method 。下面验证基于枚举创建的单例类是不会被序列化破坏的,代码结构和serialMethod()类似。

  1. private static void serialEnumMethod() {
  2. try {
  3. EnumSingleton instance1 = EnumSingleton.INSTANCE;
  4. // instance3 将从 instance1 序列化后,反序列化而来
  5. EnumSingleton instance3 = null;
  6. ByteArrayOutputStream bout = null;
  7. ObjectOutputStream out = null;
  8. try {
  9. bout = new ByteArrayOutputStream();
  10. out = new ObjectOutputStream(bout);
  11. out.writeObject(instance1);
  12.  
  13. ByteArrayInputStream bin = new ByteArrayInputStream(bout.toByteArray());
  14. ObjectInputStream in = new ObjectInputStream(bin);
  15. instance3 = (EnumSingleton) in.readObject();
  16. } catch (Exception e) {
  17. System.out.println(" - EagerSingletonPlus ------------- " + e);
  18. } finally {
  19. // close bout&out
  20. }
  21. System.out.println( "枚举自带光环,反序列化未破坏单例 : " + (instance1 == instance3)); // true
  22. } catch (Exception e) {
  23. e.printStackTrace();
  24. }
  25. }
       由此可见,enum 类自带特殊光环,不用写 readResolve() 方法就可以自动防止反序列化方式对单例的破坏,而前四种写法的单例模式则需要特殊处理。原因是在枚举类型的序列化和反序列化上,Java做了特殊的规定在枚举类型的序列化和反序列化上,Java做了特殊的规定。原文如下:
       Enum constants are serialized differently than ordinary serializable or externalizable objects. The serialized form of an enum constant consists solely of its name; field values of the constant are not present in the form. To serialize an enum constant, ObjectOutputStream writes the value returned by the enum constant’s name method. To deserialize an enum constant, ObjectInputStream reads the constant name from the stream; the deserialized constant is then obtained by calling the java.lang.Enum.valueOf method, passing the constant’s enum type along with the received constant name as arguments. Like other serializable or externalizable objects, enum constants can function as the targets of back references appearing subsequently in the serialization stream. The process by which enum constants are serialized cannot be customized: any class-specific writeObject, readObject, readObjectNoData, writeReplace, and readResolve methods defined by enum types are ignored during serialization and deserialization. Similarly, any serialPersistentFields or serialVersionUID field declarations are also ignored–all enum types have a fixed serialVersionUID of 0L. Documenting serializable fields and data for enum types is unnecessary, since there is no variation in the type of data sent.
       大概意思就是说,在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf方法根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化机制的定制的,因此禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法。 
  1. public static <T extends Enum<T>> T valueOf(Class<T> enumType,String name) {
  2. T result = enumType.enumConstantDirectory().get(name);
  3. if (result != null)
  4. return result;
  5. if (name == null)
  6. throw new NullPointerException("Name is null");
  7. throw new IllegalArgumentException(
  8. "No enum const " + enumType +"." + name);
  9. }

从代码中可以看到,代码会尝试从调用enumType这个Class对象的enumConstantDirectory()方法返回的map中获取名字为name的枚举对象,如果不存在就会抛出异常。再进一步跟到enumConstantDirectory()方法,就会发现到最后会以反射的方式调用enumType这个类型的values()静态方法,也就是上面我们看到的编译器为我们创建的那个方法,然后用返回结果填充enumType这个Class对象中的enumConstantDirectory属性。所以,JVM对序列化有保证。

Reference

  1. https://javadoop.com/post/singleton-not-single
  2. https://blog.csdn.net/whgtheone/article/details/82990139
 
 
 

Java与设计模式之单例模式(下) 安全的单例模式的更多相关文章

  1. 单例模式——Java EE设计模式解析与应用

    单例模式 目录: 一.何为单例 二.使用Java EE实现单例模式 三.使用场景 一.何为单例 确保一个类只有一个实例,并且提供了实例的一个全局访问点 1.1 单例模式类图               ...

  2. Java设计模式学习笔记,一:单例模式

    开始学习Java的设计模式,因为做了很多年C语言,所以语言基础的学习很快,但是面向过程向面向对象的编程思想的转变还是需要耗费很多的代码量的.所有希望通过设计模式的学习,能更深入的学习. 把学习过程中的 ...

  3. Java与设计模式之单例模式(上)六种实现方式

           阎宏博士在<JAVA与模式>中是这样描述单例模式的:作为对象的创建模式,单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例.这个类称为单例类.      ...

  4. 设计模式-单例模式下对多例的思考(案例:Server服务器)

    前述: 在学习单例模式后,对老师课上布置的课后作业,自然要使用单例模式,但是不是一般的单例,要求引起我的兴趣,案例是用服务器. 老师布置的要求是:服务器只有一个,但是使用这个服务器时候可以有多个对象( ...

  5. c++设计模式之单例模式下的实例自动销毁(垃圾自动回收器)

    关于C++单例模式下m_pinstance指向空间销毁问题,m_pInstance的手动销毁经常是一个头痛的问题,内存和资源泄露也是屡见不鲜,能否有一个方法,让实例自动释放. 解决方法就是定义一个内部 ...

  6. Retrofit源码设计模式解析(下)

    本文将接着<Retrofit源码设计模式解析(上)>,继续分享以下设计模式在Retrofit中的应用: 适配器模式 策略模式 观察者模式 单例模式 原型模式 享元模式 一.适配器模式 在上 ...

  7. Java的设计模式

    一.什么是设计模式: 设计模式(Design pattern)是一套被反复使用.多数人知晓的.经过分类编目的.代码设计经验的总结.使用设计模式是为了可重用代码.让代码更容易被他人理解.保证代码可靠性. ...

  8. Java EE设计模式(主要简单介绍工厂模式,适配器模式和模板方法模式)

    Java EE设计模式分为三种类型,共23种: 创建型模式:单例模式.抽象工厂模式.建造者模式.工厂模式.原型模式. 结构型模式:适配器模式.桥接模式.装饰模式.组合模式.外观模式.享元模式.代理模式 ...

  9. Java面试题全集(下)转载

    Java面试题全集(下)   这部分主要是开源Java EE框架方面的内容,包括hibernate.MyBatis.spring.Spring MVC等,由于Struts 2已经是明日黄花,在这里就不 ...

随机推荐

  1. 12.1 Mapping手动创建

    只能在index里的field不存在的时候,才能指定新field的数据类型,field有数据后,就不能再修改field的类型了 可创建的类型如下: integer double date text/s ...

  2. 压测工具wrk的编译安装与基础使用

    Linux上编译安装: [root@centos ~]# cd /usr/local/src [root@centos ~]# yum install git -y [root@centos ~]# ...

  3. Part_four:redis主从复制

    redis主从复制 1.redis主从同步 Redis集群中的数据库复制是通过主从同步来实现的 主节点(Master)把数据分发从节点(slave) 主从同步的好处在于高可用,Redis节点有冗余设计 ...

  4. mysql 数据库 规范

    目录 mysql 数据库 规范 基础规范 命名规范 表设计规范 字段设计规范 索引设计规范 SQL编写规范 行为规范 mysql 数据库 规范 基础规范 必须使用InnoDB存储引擎 解读:支持事务. ...

  5. Arm存储器

    Arm可以引出27根地址线,只能实现128MB的寻址,那么要如何实现1GB的寻址呢?答案就是使用nGCS片选线,nGCSx为低电平为选中相应的外接设备.一共八根片选线,也就是bank1,bank2-以 ...

  6. redis被攻击,怎么预防

    今天,自己的redis服务器被黑客攻击了,数据全部被删除 从图中可以看到,在db0中多了一个crackit,他就是罪魁祸首,他的值就是ssh无密码连接时需要的authorized_keys. 我们被攻 ...

  7. Python 依赖版本控制 (requirements.txt 文件生成和使用)

    requirements.txt 最好配合虚拟空间使用, 虚拟空间的使用请参考 Python 虚拟空间的使用 - 难以想象的晴朗. requirements.txt 可以保证项目依赖包版本的确定性, ...

  8. Python——字符串增加颜色

    给显示字符添加颜色: salary=int(input('\033[31;1m请输入你的工资:\033[0m')) ('\033[;1m请输入你的工资:\033[0m') 3x是给字符串改变颜色 31 ...

  9. CentOS上使用ntfs-3g挂载NTFS分区

    U盘做过系统盘,是NTFS格式的,Centos7竟然不识别,而且因为一些原因,我的服务器没有联网,只能用U盘 查过资料才知道Centos7上默认是不支持挂载NTFS格式的分区的,需要安装ntfs-3g ...

  10. Access、Trunk和Hybrid三种端口模式

    网络交换机(英语:Network switch)是一个扩大网络的器材,能为子网中提供更多的连接端口,以便连接更多的电脑. 通俗来说其起到的作用就是把一个网络端口分成多个网络端口 交换机和路由器的区别 ...