再看 Java 中的单例
此前面试遇到了单例问题,本以为已经背的滚瓜烂熟,没想到被问单例如何避免被反射和序列化破坏,虽然后来还是等到了通知,但还是复习一下单例的实现方式,并学习防止反射和序列化破坏的手段。
基本实现方式
其他相关资料中,最多的能数出八种单例实现方式,而实际上其中有些实现并不具备实际意义,在文中出现也仅是为了指出存在的问题便于引出下文。本文仅介绍有实际意义的单例实现模式。为了缩减篇幅,先给出一个后续出现代码的模板的类图:
class Singleton{
-Logger log$
-Singleton instance$
+getInstance()$ Singleton
+loadClass()$ void
+function() void
-Singleton()
}
单例类 Singleton 模板:后文中介绍具体实现方式仅给出 Singleton#instance
引用和 Singleton#getInstance
方法的内容,其他内容无变化。
public class Singleton {
private static final Logger log = LogManager.getLogger(Singleton.class);
//单例引用,不同实现方式有所不同
private static Singleton instance;
/**
* 获取单例的函数,不同实现方式有所不同
*
* @return 单例
*/
public static Singleton getInstance() {
//some code
}
/**
* 静态方法,用于触发虚拟机类加载,仅有一行日志用于观察类加载时间
*/
public static void load() {
log.debug("{} loaded", Singleton.class);
}
/**
* 单例类的功能函数,仅有一行日志
*/
public void function() {
log.debug("Singleton's instance using");
}
/**
* 私有的构造函数,仅有一行日志用于观察构造时间
*/
private Singleton() {
log.debug("Singleton's instance instantiated");
}
}
调用单例类的 Main 类:
public class Main {
public static void main(String[] args) throws Exception{
//先触发类加载
Singleton.load();
//等待一定时间
TimeUnit.SECONDS.sleep(3);
//执行单例的功能函数
Singleton.getInstance().function();
}
}
饿汉式
饿汉式具有线程安全和非 Lazy 初始化的特点,实现难度最简单。由于 JVM 的类加载是单线程的,且已加载过的类不会重复加载,所以饿汉式天生具有线程安全的特点。
由于是类加载即初始化,单例引用可添加
final
修饰。public static final Singleton instance = new Singleton(); //下面写法效果相同
/*
public static final Singleton instance; static {
instance = new Singleton();
}
*/
获取单例函数:
public static Singleton getInstance() {
return instance;
}
执行结果:
13:19:32.565 - Singleton's instance instantiated
13:19:32.569 - class cncsl.github.io.Singleton loaded
13:19:35.572 - Singleton's instance using
从日志可以看出,单例类刚加载时就调用构造函数完成了单实例的初始化。
双锁检查式
双锁检查是经常出现于面试题中的实现方式,具有线程安全和 Lazy 初始化的特点。需要自行实现线程安全的单例初始化,且要避免指令重排序导致的安全问题,实现难度较高。
为了避免指令重排序导致的线程安全问题,需要给单例引用添加
volatile
修饰:private volatile static Singleton instance;
双锁检查式最难的部分就是在获取单例的函数中进行两次非 null 判断和加锁后再初始化的过程:
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
执行结果:
13:30:07.524 - class cncsl.github.io.Singleton loaded
13:30:10.541 - Singleton's instance instantiated
13:30:10.541 - Singleton's instance using
从日志可以看出,类加载之后并没有立即初始化,实际需要调用到单例的功能函数前才进行了初始化。
静态内部类式
静态内部类式也用到了 JVM 类加载器的特性,既保证线程安全的情况下实现了 Lazy 加载。
添加一个静态内部类持有单例引用:
private static class InstanceHolder {
private static final Singleton INSTANCE = new Singleton();
}
获取单例的函数调用时才会加载静态内部类,进而触发单实例的初始化:
public static Singleton getInstance() {
return InstanceHolder.INSTANCE;
}
执行效果与双锁检查式相同,不在赘述。
枚举式
枚举式的实现是将单例类写成一个枚举,枚举值仅包含一个单例引用,再加上与业务逻辑相关的功能函数即可。由于枚举的特点,这种实现方式具有线程安全、非 Lazy 加载和防止反射、序列化破坏单例等特点。
由于改动较大附上全部代码:
public enum Singleton {
/**
* 单例枚举值
*/
INSTANCE;
/**
* 获取单例的函数
*/
public static Singleton getInstance() {
return INSTANCE;
}
/**
* 静态方法,用于触发虚拟机类加载,仅有一行日志用于观察类加载时间
*/
public static void load() {
System.out.printf("%s - %s loaded%n", LocalTime.now().toString(), Singleton.class);
}
/**
* 单例类的功能函数,仅有一行日志
*/
public void function() {
System.out.printf("%s - Singleton's instance using%n", LocalTime.now().toString());
}
/**
* 私有的构造函数,仅有一行日志用于观察构造时间
*/
Singleton() {
System.out.printf("%s - Singleton's instance instantiated%n", LocalTime.now().toString());
}
}
执行结果:
13:57:27.142 - Singleton's instance instantiated
13:57:27.154 - class cncsl.github.io.Singleton loaded
13:57:30.156 - Singleton's instance using
可以看出,枚举类加载之后立即初始化了单例对象,而三秒后执行了单例类的功能函数。
防反射和序列化破坏单例
在 Java 中,通过序列化也能创建新的对象实例,而反射能突破构造函数 private
的限制,下面介绍一下如何避免这些情况的发生。枚举式单例天生避免了这些问题,下方内容都是针对其他三种实现方式而言的。
另外,请明白一个前提,设计模式是一种设计的方式,既不是某种语言的语法约束,除了枚举方式以外、其他实现方式在有人恶意破坏的情况都无法完全确保单例。在这种情况下,需要考虑的不是如何改进现有的设计,而是找出企图通过这些手段破坏单例的人。所以下面的知识一般用于面试:当遇到如何确保单例的问题时,首先说枚举式设计方式、然后才是下面的内容。
反射手段
下方是通过反射方式破坏单例的过程:
public static void main(String[] args) {
try {
//通过getInstance()获取
Singleton one = Singleton.getInstance();
log.debug(one.hashCode());
//反射调用构造函数
Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton two = constructor.newInstance();
log.debug(two.hashCode());
log.debug(one == two);
} catch (Exception e) {
log.error("Exception: ", e);
}
}
执行结果:
15:10:48.237 - Singleton's instance instantiated
15:10:48.240 - 1159114532
15:10:48.240 - Singleton's instance instantiated
15:10:48.240 - 1832580921
15:10:48.240 - false
可以看出目前程序中以存在两个 Singleton 类的实例,单例已经被破坏。
解决方案为在单例类的构造函数中进行检查,如果单例引用不为 null 就抛出异常:
private Singleton() {
if (instance != null) {
throw new UnsupportedOperationException("不允许重复创建实例");
}
log.debug("Singleton's instance instantiated");
}
再次执行结果:
15:17:36.834 - Singleton's instance instantiated
15:17:36.836 - 1159114532
15:17:36.836 - Exception:
java.lang.reflect.InvocationTargetException: null
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) ~[?:1.8.0_261]
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62) ~[?:1.8.0_261]
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) ~[?:1.8.0_261]
at java.lang.reflect.Constructor.newInstance(Constructor.java:423) ~[?:1.8.0_261]
at cncsl.github.io.Main.main(Main.java:18) [classes/:?]
Caused by: java.lang.UnsupportedOperationException: 不允许重复创建实例
at cncsl.github.io.Singleton.<init>(Singleton.java:32) ~[classes/:?]
... 5 more
当然,攻击者可以在外部先记录一份 instance
引用,通过反射修改 instance
引用后再创建对象,这样程序中会存在两个 Singleton
实例。
序列化手段
下方是通过序列化手段破坏单例的过程:
public static void main(String[] args) {
try (ObjectOutputStream output = new ObjectOutputStream(new FileOutputStream("Singleton.temp"));
ObjectInputStream input = new ObjectInputStream(new FileInputStream("Singleton.temp"))) {
Singleton one = Singleton.getInstance();
log.debug(one.hashCode());
output.writeObject(one);
Singleton two = (Singleton) input.readObject();
log.debug(two.hashCode());
log.debug(one == two);
} catch (Exception e) {
log.error("Exception: ", e);
}
}
执行结果如下:
21:15:36.605 - Singleton's instance instantiated
21:15:36.610 - 22756955
21:15:36.619 - 1582785598
21:15:36.619 - false
可以看出,序列化读取到的对象已经是一个新的对象,单例已被破坏。
解决方案是为单例类添加如下函数:
private Object readResolve() {
return instance;
}
再次执行后可以发现已经反序列化时得到的仍然是原单例对象:
21:28:34.954 - Singleton's instance instantiated
21:28:34.956 - 22756955
21:28:34.964 - 22756955
21:28:34.964 - true
当前序列化有个前提是实现 Serializable
接口,私以为这种情况是一个错误的设计:单例类一般和业务逻辑相关、而序列化一般和封装数据用的实体对象有关,二者不应该出现在同一个类里。
再看 Java 中的单例的更多相关文章
- java中的单例设计模式
单例模式有一下特点: 1.单例类只能有一个实例. 2.单例类必须自己自己创建自己的唯一实例. 3.单例类必须给所有其他对象提供这一实例. 单例模式确保某个类只有一个实例,而且自行实例化并向整个系统提供 ...
- Java中常用来处理时间的三个类:Date、Calendar、SimpleDateFormate,以及Java中的单例设计模式:懒汉式、饿汉式以及静态内部类式
(一)java.util.Date类 1.该类有一个long类型的属性:用来存放时间,是用毫秒数的形式表示,开始的日期是从1970年1月1号 00:00:00. 2.该类的很多方法都已经过时,不 ...
- JAVA中实现单例(Singleton)模式的八种方式
单例模式 单例模式,是一种常用的软件设计模式.在它的核心结构中只包含一个被称为单例的特殊类.通过单例模式可以保证系统中,应用该模式的类一个类只有一个实例.即一个类只有一个对象实例. 基本的实现思路 单 ...
- Java设计模式之单例
一.Java中的单例: 特点: ① 单例类只有一个实例 ② 单例类必须自己创建自己唯一实例 ③ 单例类必须给所有其他对象提供这一实例 二.两种模式: ①懒汉式单例<线程不安全> 在类加载时 ...
- Java 多线程之单例设计模式
转载:https://segmentfault.com/a/1190000007504892 概念: Java中单例模式是一种常见的设计模式,单例模式的写法有好几种,这里主要介绍两种:懒汉式单例.饿汉 ...
- Spring5源码解析-Spring框架中的单例和原型bean
Spring5源码解析-Spring框架中的单例和原型bean 最近一直有问我单例和原型bean的一些原理性问题,这里就开一篇来说说的 通过Spring中的依赖注入极大方便了我们的开发.在xml通过& ...
- NopCommerce中的单例
项目中经常会遇到单例的情况.大部分的单例代码都差不多像这样定义: internal class SingletonOne { private static SingletonOne _singleto ...
- 5.2:缓存中获取单例bean
5.2 缓存中获取单例bean 介绍过FactoryBean的用法后,我们就可以了解bean加载的过程了.前面已经提到过,单例在Spring的同一个容器内只会被创建一次,后续再获取bean直接从单例 ...
- Swift中编写单例的正确方式
在之前的帖子里聊过状态管理有多痛苦,有时这是不可避免的.一个状态管理的例子大家都很熟悉,那就是单例.使用Swift时,有许多方法实现单例,这是个麻烦事,因为我们不知道哪个最合适.这里我们来回顾一下单例 ...
随机推荐
- 【Mybtais】Mybatis 插件 Plugin开发(一)动态代理步步解析
需求: 对原有系统中的方法进行'拦截',在方法执行的前后添加新的处理逻辑. 分析: 不是办法的办法就是,对原有的每个方法进行修改,添加上新的逻辑:如果需要拦截的方法比较少,选择此方法到是会节省成本.但 ...
- 【.Net Core】分析.net core在linux下内存占用过高问题
现象 随着程序运行,内存占用率越来越高,直到触发linux的OOM,程序被杀死. 分析工具 运行环境:.net core 3.1(微软的分析工具要求最低3.0,无法分析2.1的core程序,需要先改为 ...
- Pytest自动化测试-简易入门教程(02)
Pytest框架简介 Pytest是一个非常成熟的全功能的Python测试框架,主要有以下几个特点:1.简单灵活,容易上手,支持参数化2.能够支持简单的单元测试和复杂的功能测试,3.还可以用来做sel ...
- Vulnerability: Cross Site Request Forgery (CSRF)
CSRF跨站请求伪造 这是一种网络攻击方式,也被称为one-click attack或者session riding 攻击原理 CSRF攻击利用网站对于用户网页浏览器的信任,挟持用户当前已登陆的Web ...
- Charles的功能(web)
# 验证是否可以获取web端的https接口 1. 打开Charles 2.打开游览器输入数据 3. 查看Charles 4.从上图所看,能获取htpps的包数据,即可对web端进行抓包 4.char ...
- 北航OO(2020)第四单元博客作业暨学期总结
一.第四单元架构设计 1.第一次作业 我在本次作业中设置了多个储存结构:Directory,ElementsInName,ElementsInId,Cache. Directory: 顾名思义,这是个 ...
- shell脚本就是由Shell命令组成的执行文件,将一些命令整合到一个文件中,进行处理业务逻辑,脚本不用编译即可运行。它通过解释器解释运行,所以速度相对来说比较慢。
shell脚本?在说什么是shell脚本之前,先说说什么是shell. shell是外壳的意思,就是操作系统的外壳.我们可以通过shell命令来操作和控制操作系统,比如Linux中的Shell命令就包 ...
- 025.Python面向对象以及对对象的操作
一 面向对象基本概念 1.1 OOP面向对象的程序开发 用几大特征表达一类事物称为一个类,类更像是一张图纸,表达只是一个抽象概念 对象是类的具体实现,更像是由这图纸产出的具体物品,类只有一个,但是对象 ...
- python基础之包、模块、命名空间和作用域
一.模块介绍 模块就是一组功能的集合体,我们的程序可以导入模块来复用模块里的功能. 模块的作用: (1)从文件级别组织程序,更方便管理:随着程序的发展,功能越来越多,为了方便管理,我们通常将程序分成一 ...
- origin2018去掉demo水印
消除demo字样 有的origin破解完成后,使用没问题,但导出的图有demo水印.其实不需要重装,只需要下载一个补丁即可解决. 1. 把下载到的origin.exe复制到安装文件夹 2. 双击执行一 ...