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

news2025/1/11 0:37:41

为什么要问这个问题?

我们知道,单例是一种很常用的设计模式,主要作用就是节省系统资源,让对象在服务器中只有一份。但是实际开发中可能有很多人压根没有写过单例这种模式,只是看过或者为了面试去写写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.coloradmin.cn/o/845155.html

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

相关文章

springboot家政服务管理系统java家务保姆资源 jsp源代码mysql

本项目为前几天收费帮学妹做的一个项目&#xff0c;Java EE JSP项目&#xff0c;在工作环境中基本使用不到&#xff0c;但是很多学校把这个当作编程入门的项目来做&#xff0c;故分享出本项目供初学者参考。 一、项目描述 springboot家政服务管理系统 系统1权限&#xff1a;管…

Sentinel 2.0 微服务零信任的探索与实践

作者&#xff1a;涯客、十眠 从古典朴素的安全哲学谈起 网络安全现状 现在最常见的企业网络安全架构便是在企业网络边界处做安全防护&#xff0c;而在企业网络内部不做安全防范。这确实为企业的安全建设省了成本也为企业提供了一定的防护能力。但是这类比于现实情况的一个小…

解决Centos/Linux操作系统安装 uWSGI项目报错

解决linux 操作系统编译uWSGI源码报错 最近在学习在Linux操作系统中使用uWSGI项目部署django项目,在使用源码安装uWSGI项目的时候报错。 报错如下&#xff1a; In file included from plugins/python/python_plugin.c:1:0: plugins/python/uwsgi_python.h:4:20: 致命错误&…

Spring Boot整合ES的两种方式

使用Spring Data Elasticsearch Starter 在Spring Boot中整合Elasticsearch的方式之一是使用Elasticsearch的官方Spring Data Elasticsearch Starter。该Starter提供了对Elasticsearch的高级集成&#xff0c;简化了配置和管理Elasticsearch客户端。 下面是使用Spring Data E…

【SOP】最佳实践之 TiDB 业务写变慢分析

作者&#xff1a; 李文杰_Jellybean 原文来源&#xff1a; https://tidb.net/blog/d3d4465f 前言 在日常业务使用或运维管理 TiDB 的过程中&#xff0c;每个开发人员或数据库管理员都或多或少遇到过 SQL 变慢的问题。这类问题大部分情况下都具有一定的规律可循&#xff0c;…

不要在 foreach 循环里进行元素的 remove/add 操作

如果要进行remove操作&#xff0c;可以调用迭代器的 remove 方法而不是集合类的 remove 方法。因为如果列表在任何时间从结构上修改创建迭代器之后&#xff0c;以任何方式除非通过迭代器自身remove/add方法&#xff0c;迭代器都将抛出一个ConcurrentModificationException,这就…

大数据-玩转数据-Flink-Transform(上)

一、Transform 转换算子可以把一个或多个DataStream转成一个新的DataStream.程序可以把多个复杂的转换组合成复杂的数据流拓扑. 二、基本转换算子 2.1、map&#xff08;映射&#xff09; 将数据流中的数据进行转换, 形成新的数据流&#xff0c;消费一个元素并产出一个元素…

【Spring Boot】Thymeleaf模板引擎 — Thymeleaf的高级用法

Thymeleaf的高级用法 主要介绍Thymeleaf的内联、内置对象、内置变量等高级用法。 1.内联 虽然通过Thymeleaf中的标签属性已经几乎满足了开发中的所有需求&#xff0c;但是有些情况下需要在CSS或JS中访问后台返回的数据。所以Thymeleaf提供了th:inline"text/javascript/…

java+springboot+mysql日程管理系统

项目介绍&#xff1a; 使用javaspringbootmysql开发的日程管理系统&#xff0c;系统包含超级管理员、管理员、用户角色&#xff0c;功能如下&#xff1a; 超级管理员&#xff1a;管理员管理&#xff1b;用户管理&#xff1b;反馈管理&#xff1b;系统公告&#xff1b;个人信息…

解析器模式(C++)

定义 给定一个语言&#xff0c;定义它的文法的一种表示&#xff0c;并定义一种解释器&#xff0c;这个解释器使用该表示来解释语言中的句子。 应用场景 在软件构建过程中&#xff0c;如果某一特定领域的问题比较复杂&#xff0c;类似的结构不断重复出现&#xff0c;如果使用…

JavaWeb三大组件——Filter

目录 过滤器作用 注册Filter 父pom文件 pom文件 注解方式 TokenFilter LogFilter Logout 启动类 使用FilterRegistrationBean注册 结果 过滤器作用 过滤器用途主要包括&#xff1a;对用户请求进行统一认证、对用户的访问请求进行记录和审核、对用户发送的数据进行过…

UE4/5 GAS技能系统入门2 - AttributeSet

在GAS系统中对属性进行修改需要用到GE&#xff08;Gameplay Effect&#xff09;&#xff0c;而这又涉及到AttributeSet这样的概念。 AttributeSet用于描述角色的属性集合&#xff0c;如攻击力、血量、防御力等&#xff0c;与GAS系统整合度较高&#xff0c;本文就来讲一讲Attri…

【1++的数据结构】之二叉搜索树

&#x1f44d;作者主页&#xff1a;进击的1 &#x1f929; 专栏链接&#xff1a;【1的数据结构】 文章目录 一&#xff0c;什么是二叉搜索树二&#xff0c;二叉搜索树的操作及其实现2.1 插入操作及其实现2.2 查找操作及其实现2.3 删除操作及其实现 三&#xff0c;构造及其析构四…

docker版jxTMS使用指南:使用jxTMS采集数据之一

本文讲解了如何jxTMS的数据采集与处理框架并介绍了如何用来采集数据&#xff0c;整个系列的文章请查看&#xff1a;docker版jxTMS使用指南&#xff1a;4.4版升级内容 docker版本的使用&#xff0c;请查看&#xff1a;docker版jxTMS使用指南 4.0版jxTMS的说明&#xff0c;请查…

【搭建PyTorch神经网络进行气温预测】

import numpy as np import pandas as pd import matplotlib.pyplot as plt import torch import torch.optim as optim import warnings warnings.filterwarnings("ignore") %matplotlib inlinefeatures pd.read_csv(temps.csv)#看看数据长什么样子 features.head…

图像 分割 - Fast-SCNN: Fast Semantic Segmentation Network (arXiv 2019)

Fast-SCNN: Fast Semantic Segmentation Network - 快速语义分割网络&#xff08;arXiv 2019&#xff09; 摘要1. 引言2. 相关工作2.1 语义分割的基础2.2 DCNN的效率2.3 辅助任务预训练 3. 提议的Fast-SCNN3.1 动机3.2 网络架构3.2.1 学习下采样3.2.2 全局特征提取器3.2.3 特征…

侯捷 C++面向对象编程笔记——10 继承与虚函数

10 继承与虚函数 10.1 Inheritance 继承 语法&#xff1a;:public base_class_name public 只是一种继承的方式&#xff0c;还有protect&#xff0c;private 子类会拥有自己的以及父类的数据 10.1.1 继承下的构造和析构 与复合下的构造和析构相似 构造是由内而外 Container …

试图将更改推送到 GitHub,但是远程仓库已经包含了您本地没有的工作(可能是其他人提交的修改)

这通常是由于其他人或其他仓库推送到了相同的分支上&#xff0c;导致您的本地仓库和远程仓库之间存在冲突。 错误信息&#xff1a; To github.com:8upersaiyan/CKmuduo.git ! [rejected] main -> main (fetch first) error: failed to push some refs to github.com:8upers…

Webots与MATLAB联合仿真环境配置

1. 版本 系统&#xff1a;Win10 matlab版本&#xff1a;2023a webots版本&#xff1a;R2020b 2.安装 MATLAB MinGW-w64 C/C Compiler 在使用matlab写控制器之前&#xff0c;需要给matlab安装 MATLAB MinGW-w64 C/C Compiler&#xff0c;因为需要matlab与c进行交互。 下载地址…

K8S系列文章之 Kind 部署K8S的 服务发布

安装kind 下载 https://github.com/kubernetes-sigs/kind/releases/download/0.17.0/kind-linux-amd64 执行以下命令&#xff1a; mv kind-linux-amd64 /usr/local/bin/kind chmod 777 /usr/local/bin/kind 之前需要先在本地主机安装好docker yum -y install yum-utils d…