Java与设计模式之单例模式(下) 安全的单例模式
- 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 在创建实例的时候进行调用。
通过序列化机制破坏
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();
}
}
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
Java与设计模式之单例模式(下) 安全的单例模式的更多相关文章
- 单例模式——Java EE设计模式解析与应用
单例模式 目录: 一.何为单例 二.使用Java EE实现单例模式 三.使用场景 一.何为单例 确保一个类只有一个实例,并且提供了实例的一个全局访问点 1.1 单例模式类图 ...
- Java设计模式学习笔记,一:单例模式
开始学习Java的设计模式,因为做了很多年C语言,所以语言基础的学习很快,但是面向过程向面向对象的编程思想的转变还是需要耗费很多的代码量的.所有希望通过设计模式的学习,能更深入的学习. 把学习过程中的 ...
- Java与设计模式之单例模式(上)六种实现方式
阎宏博士在<JAVA与模式>中是这样描述单例模式的:作为对象的创建模式,单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例.这个类称为单例类. ...
- 设计模式-单例模式下对多例的思考(案例:Server服务器)
前述: 在学习单例模式后,对老师课上布置的课后作业,自然要使用单例模式,但是不是一般的单例,要求引起我的兴趣,案例是用服务器. 老师布置的要求是:服务器只有一个,但是使用这个服务器时候可以有多个对象( ...
- c++设计模式之单例模式下的实例自动销毁(垃圾自动回收器)
关于C++单例模式下m_pinstance指向空间销毁问题,m_pInstance的手动销毁经常是一个头痛的问题,内存和资源泄露也是屡见不鲜,能否有一个方法,让实例自动释放. 解决方法就是定义一个内部 ...
- Retrofit源码设计模式解析(下)
本文将接着<Retrofit源码设计模式解析(上)>,继续分享以下设计模式在Retrofit中的应用: 适配器模式 策略模式 观察者模式 单例模式 原型模式 享元模式 一.适配器模式 在上 ...
- Java的设计模式
一.什么是设计模式: 设计模式(Design pattern)是一套被反复使用.多数人知晓的.经过分类编目的.代码设计经验的总结.使用设计模式是为了可重用代码.让代码更容易被他人理解.保证代码可靠性. ...
- Java EE设计模式(主要简单介绍工厂模式,适配器模式和模板方法模式)
Java EE设计模式分为三种类型,共23种: 创建型模式:单例模式.抽象工厂模式.建造者模式.工厂模式.原型模式. 结构型模式:适配器模式.桥接模式.装饰模式.组合模式.外观模式.享元模式.代理模式 ...
- Java面试题全集(下)转载
Java面试题全集(下) 这部分主要是开源Java EE框架方面的内容,包括hibernate.MyBatis.spring.Spring MVC等,由于Struts 2已经是明日黄花,在这里就不 ...
随机推荐
- 普通element ui table组件的使用
1.使用基础的element ui 的table的基础使用 首先,使用前要先引用element库到项目中,可以直接引入element的js和css或者在vue项目下按需加载不同的组件 废话不多说,直接 ...
- 【openshift】在Openshift上通过yaml部署应用
在Openshift上通过yaml部署应用 1.通过直接执行yaml 通过如下命令直接执行 oc create -f nginx.yml nginx.yml apiVersion: v1 items: ...
- 【转载】C#中string类使用Replace方法来替换字符串
在C#的字符串操作过程中,有时候需要替换字符串中的某个子字符串,此时就可以使用到字符串类自带的Replace方法来实现,Replace方法将查找到所有符合被替换的子字符串,然后将之全部替换为目标字符串 ...
- Vue路由传参的几种方式
原 Vue路由传参的几种方式 2018年07月28日 23:52:40 广积粮缓称王 阅读数 12613 前言:顾名思义,vue路由传参是指嵌套路由时父路由向子路由传递参数,否则操作无效.传参方式 ...
- 微信小程序分享小程序码的生成,多参数以及参数的获取
如果本文对你有用,请爱心点个赞,提高排名,帮助更多的人.谢谢大家!❤ 如果解决不了,可以在文末进群交流. 官方文档地址:https://developers.weixin.qq.com/minipro ...
- c# Stack 类
- Mac下iTerm2配置lrzsz功能
Mac下iTerm2配置lrzsz功能 rz,sz是Linux/Unix同Windows进行ZModem文件传输的命令行工具. 优点就是不用再开一个sftp工具登录上去上传下载文件. 近期在mac上通 ...
- 第五次个人作业——Alpha测试
这个作业属于哪个课程 课程链接 这个作业要求在哪里 作业要求 团队名称 巧克力王子与六个小矮人 一.测试项目博客地址 项目名 团队名 博客地址 项目发布地址 西柚排课王 西柚排课王 https://w ...
- centos7 安装 mongodb 4.0.0
原文链接:http://www.webosss.com/article/detail/38 下载mongodb:地址:https://fastdl.mongodb.org/linux/mongodb- ...
- gensim中word2vec和其他一些向量的使用
直接上代码吧,word2vec # test from gensim.models.word2vec import Word2Vec txt_file = open('data.txt') sente ...