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

这篇具有很好参考价值的文章主要介绍了如何构造一个安全的单例?。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

为什么要问这个问题?

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

怎么构造一个单例,java,java,设计模式

序列化反序列化会怎样?

直接上代码

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);
    }
}

怎么构造一个单例,java,java,设计模式
这时候,是不是又会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是把对象头读出来
怎么构造一个单例,java,java,设计模式
通过switch case(tc)判断对象的类型,这里我们用的是OBJECT类型,魔数为0x73
怎么构造一个单例,java,java,设计模式
怎么构造一个单例,java,java,设计模式
这时候,会调用内部私有的readOrdinaryObject()方法
怎么构造一个单例,java,java,设计模式
这里就是调用我们重写的readResolve()方法啦!
怎么构造一个单例,java,java,设计模式
这就是jdk的大佬们为提供的一个hook方法,我们可以用它保证序列化和反序列化的安全。
有人一定会说,你有病,序列化一个单例!不,我没病,只是你没用到过而已
有人也会说,用枚举不就屏蔽这些问题了吗?不,如果JDK1.6的情况下是不能把枚举当做单例对象玩的。

好了,到这里就结束了吗?
不,记得给你的单例类加上final,防止被继承后重写!文章来源地址https://www.toymoban.com/news/detail-642832.html

结论

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

到了这里,关于如何构造一个安全的单例?的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • [Unity] 单例设计模式, 可供继承的单例组件模板类

    一个可供继承的单例组件模板类: 因为 Unity 是单线程的, 所以在这里没有必要使用双检索 例如你要创建一个全局的单例管理类, 可以这样使用: 尽量避免让 SingletonComponent 帮你创建组件, 因为它只是单纯的将组件创建, 并挂载到空对象上, 而不会进行任何其他行为. 如果你的组件

    2024年02月08日
    浏览(34)
  • 设计模式3:单例模式:静态内部类模式是怎么保证单例且线程安全的?

    上篇文章:设计模式3:单例模式:静态内部类单例模式简单测试了静态内部类单例模式,确实只生成了一个实例。我们继续深入理解。 静态变量什么时候被初始化? 这行代码 private static Manager instance = new Manager(); 什么时候执行? 编译期间将.java文件转为.class文件,运行期间

    2024年02月12日
    浏览(33)
  • Java 设计模式 - 单例模式 - 保证类只有一个实例

    单例模式是一种创建型设计模式,它确保一个类只有一个实例,并提供一个全局访问点来获取该实例。在某些情况下,我们需要确保一个类的实例在整个应用程序中是唯一的,这时候单例模式就非常有用。在本篇博客中,我们将详细探讨单例模式的概念、实现方式以及如何在

    2024年02月15日
    浏览(33)
  • 【多线程】线程安全的单例模式

    单例模式能保证某个类在程序中只存在 唯一 一份实例, 而不会创建出多个实例,从而节约了资源并实现数据共享。 比如 JDBC 中的 DataSource 实例就只需要一个. 单例模式具体的实现方式, 分成 “饿汉” 和 “懒汉” 两种. 类加载的同时, 创建实例. 注意: 使用 static 修饰 instanc

    2024年02月09日
    浏览(36)
  • 【Java中23种设计模式-单例模式2--懒汉式2线程安全】

    加油,新时代打工人! 简单粗暴,学习Java设计模式。 23种设计模式定义介绍 Java中23种设计模式-单例模式 Java中23种设计模式-单例模式2–懒汉式线程不安全 通过运行结果看,两个线程的地址值是相同的,说明内存空间里,创建了一个对象。

    2024年02月20日
    浏览(37)
  • kotlin实现java的单例模式

    Kotlin的5种单例模式

    2024年02月10日
    浏览(28)
  • 如何设计WIndows系统下的单例进程程序?

    为了设计一个Windows系统下的单例进程程序,你可以遵循以下步骤: 首先,确定你的应用程序只能运行一个实例。这可以通过使用互斥量(Mutex)来实现。互斥量是一种同步对象,用于控制对共享资源的独占访问。 在应用程序的启动代码中,创建一个命名的互斥量对象。命名

    2024年01月22日
    浏览(38)
  • 7种创建方式,带你理解Java的单例模式

    本文分享自华为云社区《《Java极简设计模式》第01章:单例模式(Singleton)》,作者:冰 河。 看几个单例对象的示例代码,其中有些代码是线程安全的,有些则不是线程安全的,需要大家细细品味,这些代码也是在高并发环境下测试验证过的。 代码一:SingletonExample1 这个类

    2024年02月12日
    浏览(34)
  • 如何在 Java 中从另一个构造函数调用一个构造函数?

    是否可以从另一个(在同一个类中,而不是从子类中)调用构造函数?如果是怎么办?调用另一个构造函数的最佳方法是什么(如果有几种方法可以做到)? huntsbot.com高效搞钱,一站式跟进超10+任务平台外包需求 对的,这是可能的: 要链接到特定的超类构造函数而不是同一

    2024年02月03日
    浏览(44)
  • flutter使用shared_preferences依赖库实现简单的本地数据存储,封装成一个简单的单例类,方便前端同学使用

    shared_preferences 仓库地址:shared_preferences | Flutter Package shared_preferences这个依赖库还是非常好用的,全平台支持,就像前端经常使用的localstorage一样方便,所以就想着封装成一个简单的类,方便前端同学使用。封装好的代码支持json或者数组等这种类型的存储和获取。 在utils里面

    2024年01月20日
    浏览(34)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包