如何构造一个安全的单例?

为什么要问这个问题?

我们知道,单例是一种很常用的设计模式,主要作用就是节省系统资源,让对象在服务器中只有一份。但是实际开发中可能有很多人压根没有写过单例这种模式,只是看过或者为了面试去写写demo熟悉一下。那为啥说是一种常用的模式?
其实我们用的spring管理对象生命周期,用到默认的scope就是单例。这样的场景几乎每天都在用,所以我们不需要自己手写单例了。
那么为了面试,进大厂,是不是就要刷刷文章学习学习呢?当我们刷完单例的整体结构时,会发现还是很简单的嘛,无非就是懒汉、饿汉。饿汉上来就创建,没什么难的,懒汉可能会在创建的时候线程不安全,还要防止jvm在server模式下进行指令重排,加双层锁判断就ok啦!面试很简单嘛,照着文章中的背下来就行了。
但是,你有没有遇到面试官问你,如何构造一个安全的单例?注意,是安全的。
如果你没遇到,恭喜你,面试官不想为难你,或者他没把单例玩明白。如果你遇到了,很不幸,这个面试官是个注重细节的人,而且在给你挖坑。
当然,我觉得在面试的时候这么问单例的人,可能只有我。
刚刚说了,线程不安全加锁就解决了。但是,这里安全,可不只是是线程安全,光知道线程安全没什么特殊的,无非你准备过面试。那这里还有什么猫腻?
答案:

  1. 反射攻击,导致单例变成多例,不安全了
  2. 序列化、反序列化变成多例,不安全了

这两点,下面再说具体为什么和怎么解决。先说说,作为面试官的我为什么这么问?
首先,我知道现在存在很多刷题网站,说用过的人,并不一定真的用过,只是刷了题,我要筛出真正会的人,而不是刷过题的人。
其次,一个单例,考察的不是一个模式这么简单,如果回答出这两个答案的人,我会认为,他的java基础非常好,而且考虑问题非常全面和谨慎。怎么看出来的?
基础好:我们最常用的序列化方式恐怕就是json、xml、pb、hessian等协议,很少有人用java自带的字节流序列化。用字节流序列化只有一种情况,redis存储、消息报文投递、IO编程时考虑性能,还有可能对字节进行压缩。这个时候,如果你只是对Serializable接口有所了解,知道serialVersionUID就有一些浅了。如果你知道readResolve,那证明你对java序列化理解的很透彻。
考虑问题全面和谨慎:一般的人实现单例,只是满足功能就可以了,甚至不考虑懒汉的线程安全问题。如果你考虑反射攻击带来的危害,那你在做架构方案设计时,一定是很全面和谨慎的,你的方案也是可靠的。

反射攻击是什么?

如果不使用饿汉,不使用枚举做单例,那我们要么做静态内部类,要么做双重锁+volatile来保证线程安全。同时无论是饿汉还是懒汉,只要不用枚举,我们都需要做私有构造函数。如下:

//静态内部类实现方式
private Singleton(){} //不安全的点
public Singleton getInstance(){return Instance.INSTANCE;
}
private static class Instance{private static final Singleton INSTANCE = new Singleton();
}
//双重锁实现方式
private static volatile Singleton instance = null;
private Singleton(){} //不安全的点
public Singleton getInstance(){if(instance == null){synchronized(Singleton.class){if(instance == null){instance = new Singleton();}}}
}
//饿汉实现方式
private static final Singleton INSTANCE = new Singleton();
private Singleton(){} //不安全的点
public Singleton getInstance(){return INSTANCE;
}

上面的三种实现方式,注意私有构造函数(这里加了注释)是不安全的,本意是防止被调用者直接new Singleton()创建对象设置为私有,在一般情况下,这是没问题的。
但是用心良苦的人,可能会这么调用你:

for(int i=0;i<10000000;i++){Singleton.class.newInstance();//用反射绕过私有构造函数,直接创建对象
}

这个时候,你的业务是不是会Denial of service
所以这很坑,但是你会说,我写的单例代码在java服务器内部,怎么会被人这么调用?这是不可能发生的!没错,这没问题。如果你的代码是开源的,你怎么知道那些内心黑暗的人会不会从某个http接口伪造什么东西来触发newInstance()?仔细想想,这两年有多少人被FastJSON坑的大晚上不能安心睡觉,要紧急升级代码?恐怕开发阿里爸爸FastJSON团队,也不想出现这样的状况。
所以我们要严谨!

private Singleton(){throw new IllegalStateException();
}

当然,这个时候是无法使用双重锁+volatile方式创建单例的,因为自身调用也会抛异常。所以直接用静态内部类方式解决问题。

//静态内部类实现方式
private Singleton(){if(Instance.INSTANCE!=null){throw new IllegalStateException();//这下安全了}
} 
public Singleton getInstance(){return Instance.INSTANCE;
}
private static class Instance{private static final Singleton INSTANCE = new Singleton();
}

在这里插入图片描述

序列化反序列化会怎样?

直接上代码

public class Singleton implements Serializable{private Singleton() {if (Instance.INSTANCE != null) {//这里我可是防止了反射攻击哦!throw new IllegalStateException();}}public static Singleton getInstance() {return Instance.INSTANCE;}private static class Instance {private static final Singleton INSTANCE = new Singleton();}public static void main(String[] args) throws IOException, ClassNotFoundException {File file = new File("~/Desktop/Singleton.bin");Singleton singleton = Singleton.getInstance();ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file));oos.writeObject(singleton);ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));Singleton singleton1 = (Singleton) ois.readObject();System.out.println(singleton == singleton1);}
}

在这里插入图片描述
这时候,是不是又会Denial of service
为啥会这样?其实很简单,只要实现Serializable接口的类对象,ObjectOutputStream会毫不犹豫的吐成字节,或者读回来,它才不管你是不是单例,当然它也不知道内存中有这么一个单例,直接在内存中创建对象了。
如何解决这个问题?

public class Singleton implements Serializable{private Singleton() {if (Instance.INSTANCE != null) {throw new IllegalStateException();}}public static Singleton getInstance() {return Instance.INSTANCE;}//实现readResolve接口就ok了private Object readResolve() throws ObjectStreamException {return Instance.INSTANCE;}private static class Instance {private static final Singleton INSTANCE = new Singleton();}public static void main(String[] args) throws IOException, ClassNotFoundException {File file = new File("/Users/baodi/Desktop/Singleton.bin");Singleton singleton = Singleton.getInstance();ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file));oos.writeObject(singleton);ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));Singleton singleton1 = (Singleton) ois.readObject();System.out.println("源对象singleton == 反序列化对象 singleton1吗?"+(singleton == singleton1));}

实现readResolve,返回静态内部类的对象就可以了。
看看源码(我用的jdk1.8,1.6、1.7也一样)ObjectOutputStream.readObject()中会调用内部私有方法readObject0(),其中byte tc是把对象头读出来
在这里插入图片描述
通过switch case(tc)判断对象的类型,这里我们用的是OBJECT类型,魔数为0x73
在这里插入图片描述
在这里插入图片描述
这时候,会调用内部私有的readOrdinaryObject()方法
在这里插入图片描述
这里就是调用我们重写的readResolve()方法啦!
在这里插入图片描述
这就是jdk的大佬们为提供的一个hook方法,我们可以用它保证序列化和反序列化的安全。
有人一定会说,你有病,序列化一个单例!不,我没病,只是你没用到过而已
有人也会说,用枚举不就屏蔽这些问题了吗?不,如果JDK1.6的情况下是不能把枚举当做单例对象玩的。

好了,到这里就结束了吗?
不,记得给你的单例类加上final,防止被继承后重写!

结论

  1. 面试不只是刷刷题就ok了
  2. 请认真的对待你写过的每一个代码,因为你很可能把别人坑了,比如FastJSON
  3. 做技术要刨根问底,网上的资料都是说如何序列化不安全的问题,并没有给出ObjectOutputStream.readObject()执行原理分析,不信你搜
  4. 严谨、夯实基础

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/27470.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

C语言代码的x86-64汇编指令分析过程记录

先通过Xcode创建一个terminal APP&#xff0c;语言选择C。代码如下&#xff1a; #include <stdio.h>int main(int argc, const char * argv[]) {int a[7]{1,2,3,4,5,6,7};int *ptr (int*)(&a1);printf("%d\n",*(ptr));return 0; } 在return 0处打上断点&…

Linux usb设备固定端口号

Linux usb设备固定端口号 一:/sys/bus/usb/devices/二:设备信息三:固定usb设备名方法 一:/sys/bus/usb/devices/ 信息显示如下 1-0:1.0 1&#xff1a;表示 1 号总线&#xff0c;或者说 1 号 Root Hub0&#xff1a;表示端口号1&#xff1a;表示配置号0&#xff1a;表示接口号命…

错过NFT投资,你可能会后悔吗?

在过去的几年里&#xff0c;非同质化代币&#xff08;NFT&#xff09;以其独特性和创新性&#xff0c;迅速成为数字资产领域的热门话题。NFT代表了数字资产的所有权和独特性&#xff0c;为艺术、音乐、游戏和虚拟世界等领域提供了全新的可能性。然而&#xff0c;对于那些错过了…

冠达管理投资前瞻:三星加码机器人领域 大信创建设提速

上星期五&#xff0c;沪指高开高走&#xff0c;盘中一度涨超1%打破3300点&#xff0c;但随后涨幅收窄&#xff1b;深成指、创业板指亦强势震动。截至收盘&#xff0c;沪指涨0.23%报3288.08点&#xff0c;深成指涨0.67%报11238.06点&#xff0c;创业板指涨0.95%报2263.37点&…

gin框架学习

文章目录 配置go环境实现一个简单的web响应服务验证功能gin增加页面以及传递数据 配置go环境 去go官网下载对应的版本 go下载地址 tar -C /usr/local -xzf go1.4.linux-amd64.tar.gz 我们可以编辑 ~/.bash_profile 或者 /etc/profile&#xff0c;并将以下命令添加该文件的末…

【排序算法】python之冒泡,选择,插入,快速,归并

参考资料&#xff1a; 《Python实现5大排序算法》《六大排序算法&#xff1a;插入排序、希尔排序、选择排序、冒泡排序、堆排序、快速排序》 --代码似乎是C语言 ———————— 本文介绍5种常见的排序算法和基于Python实现&#xff1a; 冒泡排序&#xff08;Bubble Sort&am…

pocky-request网络请求插件

插件下载地址&#xff1a;https://ext.dcloud.net.cn/plugin?id468 插件&#xff1a;https://www.yuque.com/pocky/aaeyux/irx7u0#Oosbz 使用教程&#xff1a; 下载插件main.js中配置&#xff1a; // 导入 import axiosRequest from ./js_sdk/pocky-request/pocky-request…

鉴源实验室丨SOME/IP协议安全攻击

作者 | 张昊晖 上海控安可信软件创新研究院工控网络安全组 来源 | 鉴源实验室 社群 | 添加微信号“TICPShanghai”加入“上海控安51fusa安全社区” 01 引 言 随着汽车行业对于数据通信的需求不断增加&#xff0c;SOME/IP作为支持汽车以太网进程和设备间通信的一种通信协议应…

【Git】Git切换地址

如何切换git代码地址&#xff1f; 1、查看当前远程 url git remote -v执行命令后&#xff0c;可以看见当前有2个URL。 远程 URL 在一般情况下有两个&#xff0c;分别是 fetch 和 push。 fetch URL 是用于从远程仓库获取最新版本的数据。当您运行 git fetch 命令时&#xf…

Oracle-ORA-00600:[ktspffbmb:objdchk_kcbnew_3]

问题背景: 应用执行存储过程报错ORA-00600: 内部错误代码, 参数: [ktspffbmb:objdchk_kcbnew_3], [0], [3303775], [4], [], [], [], [], [], [], [], []&#xff0c;导致过程无法正常执行 ORA-00600: 内部错误代码, 参数: [ktspffbmb:objdchk_kcbnew_3], [0], [3303775], [4]…

结构体和 Json 相互转换(序列化反序列化)

关于 JSON 数据 JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式。易于人阅读和编写。同时也 易于机器解析和生成。RESTfull Api 接口中返回的数据都是 json 数据。 Json 的基本格式如下&#xff1a; { "a": "Hello", "b": "…

机器视觉赛道持续火热,深眸科技坚持工业AI视觉切入更多应用领域

随着深度学习等算法的突破、算力的不断提升以及海量数据的持续积累&#xff0c;人工智能逐渐从学术界向工业界落地。而机器视觉作为人工智能领域中一个正在快速发展的分支&#xff0c;广泛应用于工业制造的识别、检测、测量、定位等场景&#xff0c;相较于人眼&#xff0c;在精…

学习才是测试猿的永动力!超详细的 pytest 钩子函数 之初始钩子和引导钩子来啦

前 言 前几篇文章介绍了 pytest 点的基本使用&#xff0c;学完前面几篇的内容基本上就可以满足工作中编写用例和进行自动化测试的需求。从这篇文章开始会陆续给大家介绍 pytest 中的钩子函数&#xff0c;插件开发等等。仔细去看过 pytest 文档的小伙伴&#xff0c;应该都有发现…

Visual Studio 2022的MFC框架——应用程序向导

我是荔园微风&#xff0c;作为一名在IT界整整25年的老兵&#xff0c;今天我们来重新审视一下Visual Studio 2022开发工具下的MFC框架知识。 MFC(Microsoft Foundation Class&#xff0c;微软基础类库&#xff09;是微软为了简化程序员的开发工作所开发的一套C类的集合&#xf…

RabbitMQ的安装

RabbitMQ的安装 1、Windows环境下的RabbitMQ安装步骤 使用的版本&#xff1a;otp_win64_23.2 rabbitmq-server-3.8.16 版本说明&#xff1a;https://www.rabbitmq.com/which-erlang.html#compatibility-matrix 1.1 下载并安装erlang RabbitMQ 服务端代码是使用并发式语言…

【vim 学习系列文章 4 - vim与系统剪切板之间的交互】

文章目录 背景1.1.1 vim支持clipboard 检查1.1.2 vim的寄存器 上篇文章&#xff1a;【vim 学习系列文章 3 - vim 选中、删除、复制、修改引号或括号内的内容】 背景 从vim中拷贝些文字去其它地方粘贴&#xff0c;都需要用鼠标选中vim的文字后&#xff0c;Ctrlc、Ctrlv&#x…

怎么绘制汤姆索亚历险记思维导图?掌握这几个绘制步骤就可以

怎么绘制汤姆索亚历险记思维导图&#xff1f;如果你正在为学习汤姆索亚历险记而感到困惑&#xff0c;或者你想要更好地理解小说中的人物关系、情节和舞台背景&#xff0c;那么一个清晰的思维导图就可以帮助你梳理思路。那么下面就给大家介绍一下绘制步骤。 在进行思维导图绘制的…

docker容器监控:Cadvisor +Prometheus+Grafana的安装部署

目录 Cadvisor PrometheusGrafana的安装部署 一、安装docker&#xff1a; 1、安装docker-ce 2、阿里云镜像加速器 3、下载组件镜像 4、创建自定义网络 二、部署Cadvisor 1、被监控主机上部署Cadvisor容器 2、访问cAdvisor页面 三、安装prometheus 1、部署Prometheus…

Clion开发Stm32之存储模块(W25Q64)驱动编写

前言 涵盖之前文章: Clion开发STM32之HAL库SPI封装(基础库) W25Q64驱动 头文件 #ifndef F1XX_TEMPLATE_MODULE_W25Q64_H #define F1XX_TEMPLATE_MODULE_W25Q64_H#include "sys_core.h" /* Private typedef ---------------------------------------------------…

LNMP搭建

LNMP&#xff1a;目前成熟的企业网站的应用模式之一&#xff0c;指的是一套协同工作的系统和相关软件 能够提供静态页面服务&#xff0c;也可以提供动态web服务。 这是一个缩写 L linux系统&#xff0c;操作系统。 N nginx网站服务&#xff0c;也可也理解为前端&#xff0c…