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

通过反射破坏单例

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

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

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

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

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

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

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

通过序列化机制破坏

     
       下面,我们再说说另一种破解方法:序列化、反序列化。我们知道,序列化是将 java 对象转换为字节流,反序列化是从字节流转换为 java 对象。下面以饿汉式单例为例,验证反序列化会生成新的单例实例。
   private static void serialMethod() {
try {
EagerSingleton instance1 = EagerSingleton.getInstance(); // instance3 将从 instance1 序列化后,反序列化而来
EagerSingleton instance3 = null;
ByteArrayOutputStream bout = null;
ObjectOutputStream out = null;
try {
bout = new ByteArrayOutputStream();
out = new ObjectOutputStream(bout);
out.writeObject(instance1); ByteArrayInputStream bin = new ByteArrayInputStream(bout.toByteArray());
ObjectInputStream in = new ObjectInputStream(bin);
instance3 = (EagerSingleton) in.readObject();
} catch (Exception e) {
System.out.println(" -------------- " + e);
} finally {
// close bout&out
}
System.out.println( "序列化是否破坏了单例 : " + (instance1 == instance3));
} catch (Exception e) {
e.printStackTrace();
}
}

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

import java.io.ObjectStreamException;
import java.io.Serializable; /**
* 序列化的饿汉式单例,线程安全
*/
public class EagerSingletonPlus implements Serializable { private static final EagerSingletonPlus instance = new EagerSingletonPlus();
// 私有化构造方法 private EagerSingletonPlus() {
}
public static EagerSingletonPlus getInstance() {
return instance;
}
/**
* 看这里,新增
*/
public Object readResolve() throws ObjectStreamException {
return instance;
} // 序列化,新增
private static final long serialVersionUID = -3006063981632376005L;
}

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

   private static void serialEnumMethod() {
try {
EnumSingleton instance1 = EnumSingleton.INSTANCE;
// instance3 将从 instance1 序列化后,反序列化而来
EnumSingleton instance3 = null;
ByteArrayOutputStream bout = null;
ObjectOutputStream out = null;
try {
bout = new ByteArrayOutputStream();
out = new ObjectOutputStream(bout);
out.writeObject(instance1); ByteArrayInputStream bin = new ByteArrayInputStream(bout.toByteArray());
ObjectInputStream in = new ObjectInputStream(bin);
instance3 = (EnumSingleton) in.readObject();
} catch (Exception e) {
System.out.println(" - EagerSingletonPlus ------------- " + e);
} finally {
// close bout&out
}
System.out.println( "枚举自带光环,反序列化未破坏单例 : " + (instance1 == instance3)); // true
} catch (Exception e) {
e.printStackTrace();
}
}
       由此可见,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等方法。 
   public static <T extends Enum<T>> T valueOf(Class<T> enumType,String name) {
T result = enumType.enumConstantDirectory().get(name);
if (result != null)
return result;
if (name == null)
throw new NullPointerException("Name is null");
throw new IllegalArgumentException(
"No enum const " + enumType +"." + name);
}

从代码中可以看到,代码会尝试从调用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. java后台获取微信小程序openid

    一.jar包准备 1.在网盘下载 链接:https://pan.baidu.com/s/15HAAWOg_yn768g4s9IrcPg 提取码:hgj0 二.在pom文件中添加依赖 1.将外部的引入的 ...

  2. AppRTC服务搭建(测试)

    提供一个在线的webrtc服务器测试,需要的朋友看看.https://www.webrtcserver.cn/ 服务器搭建环境各有不同在此参考前人经验差试一下. 运行AppRTC需要使用Google ...

  3. React Native 开发豆瓣评分(二)路由配置

    路由管理使用官方推荐的 React Navigation; 配置环境 安装相关依赖 yarn add react-navigation react-native-gesture-handler Lin ...

  4. elasticsearch 7版本 基础操作

    elasticsearch 7版本 基础操作 首先我们浏览器http://localhost:5601/进入 kibana里的Console中输入 首先让我们在 Console 中输入: PUT t1 ...

  5. 如何为UEditor设置默认值

    // 初始化UEditor var ue = UE.getEditor('editor'); ue.ready(function() { //设置默认值 ue.setContent('默认值....' ...

  6. p3.BTC-协议

    数字货币是文件,难伪造,但是容易复制,不像实体货币,花出去就没了,数字货币存在double spending attack,双花攻击. 去中心化的货币,需要解决两个问题: 1.货币的发行 挖矿 2.交 ...

  7. Apache实验-目录别名

    一.作用介绍 在一些情况下,我们的资源文件都在非/var/www/html目录下,例如/var/www/html/sohu.这样的话我们在输入网址的时候就需要在网站根目录下再输入完整的目录.所以我们可 ...

  8. Beta版本冲刺

    一.团队成员 团队名称 西柚排课王 项目名称 易奇排排课系统 团队成员 秦傲明 201731062308 韩浩 201731062319 黄青松 201731062322 王越豪 2017310623 ...

  9. elementui 多组件表单验证

      最近在做管理后台,vue2.0基于elementui框架进行开发. elementui的api中表单验证都是单个vue文件的验证.而我的保存按钮放在了父组件了,验证对象为三个子组件我的灵机一动 想 ...

  10. python 单元测试(unittest)

    自动化测试在各大互联网公司全面铺开,那么针对于自动化测试好的设计思想有哪些呢?.....今天我们共同探讨下Unittest之数据驱动(DDT是 “Data-Driven Tests”的缩写). 对于接 ...