如何构造一个安全的单例?
为什么要问这个问题?
我们知道,单例是一种很常用的设计模式,主要作用就是节省系统资源,让对象在服务器中只有一份。但是实际开发中可能有很多人压根没有写过单例这种模式,只是看过或者为了面试去写写demo熟悉一下。那为啥说是一种常用的模式?
其实我们用的spring管理对象生命周期,用到默认的scope就是单例。这样的场景几乎每天都在用,所以我们不需要自己手写单例了。
那么为了面试,进大厂,是不是就要刷刷文章学习学习呢?当我们刷完单例的整体结构时,会发现还是很简单的嘛,无非就是懒汉、饿汉。饿汉上来就创建,没什么难的,懒汉可能会在创建的时候线程不安全,还要防止jvm在server模式下进行指令重排,加双层锁判断就ok啦!面试很简单嘛,照着文章中的背下来就行了。
但是,你有没有遇到面试官问你,如何构造一个安全的单例?注意,是安全的。
如果你没遇到,恭喜你,面试官不想为难你,或者他没把单例玩明白。如果你遇到了,很不幸,这个面试官是个注重细节的人,而且在给你挖坑。
当然,我觉得在面试的时候这么问单例的人,可能只有我。
刚刚说了,线程不安全加锁就解决了。但是,这里安全,可不只是是线程安全,光知道线程安全没什么特殊的,无非你准备过面试。那这里还有什么猫腻?
答案:
- 反射攻击,导致单例变成多例,不安全了
- 序列化、反序列化变成多例,不安全了
这两点,下面再说具体为什么和怎么解决。先说说,作为面试官的我为什么这么问?
首先,我知道现在存在很多刷题网站,说用过的人,并不一定真的用过,只是刷了题,我要筛出真正会的人,而不是刷过题的人。
其次,一个单例,考察的不是一个模式这么简单,如果回答出这两个答案的人,我会认为,他的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,防止被继承后重写!
结论
- 面试不只是刷刷题就ok了
- 请认真的对待你写过的每一个代码,因为你很可能把别人坑了,比如FastJSON
- 做技术要刨根问底,网上的资料都是说如何序列化不安全的问题,并没有给出ObjectOutputStream.readObject()执行原理分析,不信你搜
- 严谨、夯实基础
相关文章:

如何构造一个安全的单例?
为什么要问这个问题? 我们知道,单例是一种很常用的设计模式,主要作用就是节省系统资源,让对象在服务器中只有一份。但是实际开发中可能有很多人压根没有写过单例这种模式,只是看过或者为了面试去写写demo熟悉一下。那…...

单片机开发 esp8266
一、固件界面 二、项目介绍 固件名称:esp8266-universalboard v1.0 提供商: 半条虫(466814195) 下载:esp8266-universalboard.bin 源码地址:Gitlab...
Linux 查看版本和用户权限提升实践心得
文章目录 linux (Ubuntu内核)查看版本版本信息解释内置yum工具?用户权限提升操作步骤 查看deepin系统的版本和其debian的版本遇到的问题:deepin-release文件不存在 linux (Ubuntu内核)查看版本 使用lsb_release命令: lsb_release -a该命令将…...
多线程编程5:线程同步和进程通信(C++11和linux)
常见的线程同步 linux: 互斥锁:实现共享资源的串行访问,有三个版本普通锁(默认属性),检错锁(可以防止相同线程重复加锁)和递归锁(相同线程可以重复加锁)条件变量:配合互斥锁使用,实现线程之间的通信&#…...
tensorrt官方int8量化方法汇总
原理及操作 量化的基本原理及流程可参看懂你的神经网络量化教程:第一讲、量化番外篇、TensorRT中的INT8、tensorRT int8量化示例代码 Tensorrt 方式1:trtexec(PTQ的一种) int8量化 trtexec --onnxXX.onnx --saveEnginemodel.…...

21、p6spy输出执行SQL日志
文章目录 1、背景2、简介3、接入3.1、 引入依赖3.2、修改database参数:3.3、 创建P6SpyLogger类,自定义日志格式3.4、添加spy.properties3.5、 输出样例 4、补充4.1、参数说明 1、背景 在开发的过程中,总希望方法执行完了可以看到完整是sql语…...

实力认证!TDengine 入选 Gartner 中国数据分析与人工智能技术成熟度曲线
近日,国际权威研究机构 Gartner 发布了《2023 年中国数据分析及人工智能技术成熟度曲线》(即《Hype Cycle for Data, Analytics and AI in China, 2023》)报告,TDengine 成功入选实时数据管理领域代表产品。 作为评估全球新技术成…...

如何将jar包部署到宝塔
尝试多种方式上传,但启动一直失败,这种方式亲测是好使的 项目内修改位置 在pom.xml文件中将mysql的scope改成provided,如果是固定的版本号会出现问题 之后就可以打包啦,直接点击maven中的package 找到打包文件的位置ÿ…...

el-tree-select那些事
下拉菜单树形选择器 用于记录工作及日常学习涉及到的一些需求和问题 vue3 el-tree-select那些事 1、获取el-tree-select选中的任意层级的节点对象 1、获取el-tree-select选中的任意层级的节点对象 1-1数据集 1-2画面 1-3代码 1-3-1画面代码 <el-tree-selectv-model"s…...
分布式任务调度框架之开山鼻祖:Quartz
1.简介 最近我司上线使用了分布式任务调度框架:XXL-JOB,方便对任务的管理控制。本来一开始就想讲述一下该框架,但是在学习了解过程中发现该框架式基于Quartz思想开发实现的,Quartz 是一个很火的开源任务调度框架,完全…...

Django实现音乐网站 ⑺
使用Python Django框架制作一个音乐网站, 本篇主要是后台对歌手原有实现功能的基础上进行优化处理。 目录 新增编辑 表字段名称修改 隐藏单曲、专辑数 姓名首字母 安装xpinyin 获取姓名首字母 重写保存方法 列表显示 图片显示处理 引入函数 路径改为显示…...
PO、DTO和VO的区别
PO:Persistence Object,是持久化对象,也叫实体对象(Entity),是对象到关系表的映射。在关系型数据库中,每张表对应着一个PO,PO 中的属性值与表中数据一一对应,通过数据访问…...
ReadableStream流的js处理
需求 今天接了一个gpt的项目。需要对接接口。后端返回的数据是ReadableStream类型的。记录下如何处理ReadableStream类型的数据 代码 const response ... // response 是服务端返回的数据// 创建了一个数据读取器 const reader response.getReader(); // 创建了一个文本解…...

图扑软件入选 2023 中国信通院“铸基计划”全景图
7 月 27 日,由中国信通院主办的“2023 数字生态发展大会”暨中国信通院“铸基计划”年中会议在北京召开。本次大会重磅发布了《高质量数字化转型产品及服务全景图(2023 上半年度)》。图扑软件凭借自研 HT for Web 数字孪生可视化产品成功入选…...
Go Fyne:一种现代的,跨平台的 GUI 开发工具集
Fyne 是一个用 Go 语言写的,提供丰富,现代化的用户界面(UI)的开发工具集。Fyne 提供了一种简单而直观的方式来构建应用程序,并且注意到它是跨平台的,可以在 Windows,macOS,Linux&…...

React Dva 操作models中的subscriptions讲述监听
接下来 我们来看一个models的属性 之前没有讲到的subscriptions 我们可以在自己有引入的任意一个models文件中这样写 subscriptions: {setup({ dispatch, history }) {console.log(dispatch);}, },这样 一进来 这个位置就会触发 这里 我们可以写多个 subscriptions: {setup…...

标准化归一化 batch norm, layer norm, group norm, instance norm
Layer Normalization - EXPLAINED (in Transformer Neural Networks) Layer Normalization - EXPLAINED (in Transformer Neural Networks) 0~4min:什么是multi-head attention 5~7min:layer norm图示 7~9min:公式举例layer norm 9:54-end:layer norm的代码示例 group n…...

Zabbix监控系统详解及配置
前言 作为一个运维,需要会使用监控系统查看服务器状态以及网站流量指标,利用监控系统的数据去了解上线发布的结果,和网站的健康状态。利用一个优秀的监控软件,我们可以: 通过一个友好的界面进行浏览整个网站所有的服务…...

【100天精通python】Day29:文件与IO操作_XML文件处理
目录 专栏导读 一、XML文件概述 1. 标签和元素 2. 嵌套结构 3. 属性 4. 命名空间 5. CDATA节 6. 注释 7. 验证与验证语言 8. 扩展性 二、XML文件处理常见操作 1. 解析XML文件 2. 创建和编辑XML文件 3. 修改XML文件 4. 查询XML元素 5 遍历XML元素 6. 删除XML元…...

人工智能的未来:探索下一代生成模型
推荐:使用 NSDT场景编辑器 助你快速搭建可编辑的3D应用场景 生成式 AI 目前能够做什么,以及探索下一波生成式 AI 模型需要克服的当前挑战? 如果你跟上科技世界的步伐,你就会知道生成式人工智能是最热门的话题。我们听到了很多关于…...
OpenLayers 可视化之热力图
注:当前使用的是 ol 5.3.0 版本,天地图使用的key请到天地图官网申请,并替换为自己的key 热力图(Heatmap)又叫热点图,是一种通过特殊高亮显示事物密度分布、变化趋势的数据可视化技术。采用颜色的深浅来显示…...

2.Vue编写一个app
1.src中重要的组成 1.1main.ts // 引入createApp用于创建应用 import { createApp } from "vue"; // 引用App根组件 import App from ./App.vue;createApp(App).mount(#app)1.2 App.vue 其中要写三种标签 <template> <!--html--> </template>…...
渲染学进阶内容——模型
最近在写模组的时候发现渲染器里面离不开模型的定义,在渲染的第二篇文章中简单的讲解了一下关于模型部分的内容,其实不管是方块还是方块实体,都离不开模型的内容 🧱 一、CubeListBuilder 功能解析 CubeListBuilder 是 Minecraft Java 版模型系统的核心构建器,用于动态创…...

Mac软件卸载指南,简单易懂!
刚和Adobe分手,它却总在Library里给你写"回忆录"?卸载的Final Cut Pro像电子幽灵般阴魂不散?总是会有残留文件,别慌!这份Mac软件卸载指南,将用最硬核的方式教你"数字分手术"࿰…...

IoT/HCIP实验-3/LiteOS操作系统内核实验(任务、内存、信号量、CMSIS..)
文章目录 概述HelloWorld 工程C/C配置编译器主配置Makefile脚本烧录器主配置运行结果程序调用栈 任务管理实验实验结果osal 系统适配层osal_task_create 其他实验实验源码内存管理实验互斥锁实验信号量实验 CMISIS接口实验还是得JlINKCMSIS 简介LiteOS->CMSIS任务间消息交互…...
基于matlab策略迭代和值迭代法的动态规划
经典的基于策略迭代和值迭代法的动态规划matlab代码,实现机器人的最优运输 Dynamic-Programming-master/Environment.pdf , 104724 Dynamic-Programming-master/README.md , 506 Dynamic-Programming-master/generalizedPolicyIteration.m , 1970 Dynamic-Programm…...
rnn判断string中第一次出现a的下标
# coding:utf8 import torch import torch.nn as nn import numpy as np import random import json""" 基于pytorch的网络编写 实现一个RNN网络完成多分类任务 判断字符 a 第一次出现在字符串中的位置 """class TorchModel(nn.Module):def __in…...
return this;返回的是谁
一个审批系统的示例来演示责任链模式的实现。假设公司需要处理不同金额的采购申请,不同级别的经理有不同的审批权限: // 抽象处理者:审批者 abstract class Approver {protected Approver successor; // 下一个处理者// 设置下一个处理者pub…...

安宝特案例丨Vuzix AR智能眼镜集成专业软件,助力卢森堡医院药房转型,赢得辉瑞创新奖
在Vuzix M400 AR智能眼镜的助力下,卢森堡罗伯特舒曼医院(the Robert Schuman Hospitals, HRS)凭借在无菌制剂生产流程中引入增强现实技术(AR)创新项目,荣获了2024年6月7日由卢森堡医院药剂师协会࿰…...

MacOS下Homebrew国内镜像加速指南(2025最新国内镜像加速)
macos brew国内镜像加速方法 brew install 加速formula.jws.json下载慢加速 🍺 最新版brew安装慢到怀疑人生?别怕,教你轻松起飞! 最近Homebrew更新至最新版,每次执行 brew 命令时都会自动从官方地址 https://formulae.…...