【如何破坏单例模式(详解)】

✅如何破坏单例模式

💡典型解析

单例模式主要是通过把一个类的构造方法私有化,来避免重复创建多个对象的。那么,想要破坏单例,只要想办注能够执行到这个私有的构造方法就行了。

✅拓展知识仓

一般来说做法有使用反射及使用反序列化都可以破坏单例。

我们先通过双重校验锁的方式创建一个单例,后文会通过反射及反序列化的方式尝试破坏这个单例。

java 复制代码
package com.yangxiaoyuan;

import java.io.Serializable;

/**
 * Created by yangxiaoyuan on 23/12/24
 * 使用双重校验锁方式实现单例
*/

public class Singleton implements Serializable {
	private volatile static Singleton singleton;
	private Singleton () {
		
	}

	public static Singleton getsingleton()  {
		if (singleton == null) {
			synchronized (Singleton.class)  {
				if (singleton == null) {
					singleton = new Singleton();
				}
			}
		}

		return singleton;
	}	
}

✅反射破坏单例

我们尝试通过反射技术,来破坏单例:

java 复制代码
Singleton singleton1 = Singleton.getSingleton();

//通过反射获取到构造函数
Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
//将构造函数设置为可访问类型
constructor.setAccessible(true);
//调用构造函数的newInstance创建一个对象
Singleton singleton2 = constructor.newInstance();
//判断反射创建的对象和之前的对象是不是同一个对象
System.out.println(s1 == s2);

以上代码,输出结果为false,也就是说通过反射技术,我们给单例对象创建出来了一个 "兄弟" 。

setAccessible(true),使得反射对象在使用时应该取消Java 语言访检查,使得私有的构造函数能够被访问。

✅反序列化破坏单例

我们尝试通过序列化+反序列化来破坏一下单例:

java 复制代码
package com.yangxiaoyuan;

import java.io.*;

public class SerializableDemo1 {
	//为了便于理解,忽略关闭流操作及删除文件操作。真正编码时千万不要忘记
	//Exception直接抛出


	public static void main(String  args) throws IOException, ClassNotFoundException {
		
		//Write Obj to file
		ObjectOutputStream oos = new ObjectOutputStream(new File0utputStream("tempFile"));
		oos.writeObject(Singleton.getsingleton());
		//Read Obi from file
		File file = new File("tempFile");
		ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
		Singleton newInstance = (Singleton) ois.readObject();
		//判断是否是同一个对象
		System.out.println(newInstance == Singleton.getSingleton());
	}
}

//false

输出结构为false,说明:

通过对Singleton的序列化与反序列化得到的对象是一个新的对象,这就破坏了Singleton的单例性。

这里,在介绍如何解决这个问题之前,我们先来深入分析一下,为什么会这样?在反序列化的过程中到底发生了什么。

✅ObjectlnputStream

对象的序列化过程通过ObjectOutputStream和ObiectlnputStream来实现的,那么带着刚刚的问题,分析一下ObjectlnputStream 的 readobject 方法执行情况到底是怎样的。

为了节省篇幅,这里给出ObiectlnputStream的 readobject 的调用栈:

这里看一下重点代码,readOrdinaryObject 万法的代码片段: code 3

java 复制代码
private Object readOrdinaryObject(boolean unshared) throws IOException {
	//此处省略部分代码

	Object obj;
	try {
		obj = desc.isInstantiable() ? desc.newInstance() : null;
	} catch (Exception ex)  {
		throw (IOException) new InvalidClassException(desc .forClass().getName(),
"unable to create instance").initCause(ex);
	}

	//此处省略部分代码

	if (obj != null && handles.lookupException(passHandle) == null && desc.hasReadResolveMethod()) {
		Object rep = desc.invokeReadResolve(obj);
		if (unshared && rep.getClass().isArray()) {
			rep = cloneArray(rep);
		}
		if (rep != obj) {
			handles.setObject(passHandle, obj = rep);
		}
	}
	return obj;
}

code 3 中主要贴出两部分代码。先分析第一部分:

code3.1

java 复制代码
Object obj;
	try {
		obj = desc.isInstantiable() ? desc.newInstance() : null;
	} catch (Exception ex)  {
		throw (IOException) new InvalidClassException(desc .forClass().getName(),
"unable to create instance").initCause(ex);
	}

这里创建的这个obj对象,就是本方法要返回的对象,也可以暂时理解为是ObjectlnputStream的 readobject 返回的对象。

![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/d9179a634e2a462dafeaf5c696d1a6f7.png#pic_center![在这里插入图片描述](https://file.jishuzhan.net/article/1739171315354439682/0cf8e7ab34ab6070aaadf09072f2eb35.webp)

isInstantiable : 如果一个serializable/externalizable的类可以在运行时被实例化,那么该方法就返回true。针对serializable和externalizable我会在其他文章中介绍。

desc.newInstance:该方法通过反射的方式新建一个对象。

然后看一下 newInstance 的源码:

java 复制代码
public T newInstance(Object ... initargs) throws InstantiationException, IllegalAccessException,
IllegalArgumentException,InvocationTargetException {
	if (!override) {
		if (!Reflection.quickCheckMemberAccess(clazz,modifiers)) {
			Class<?> caller = Reflection.getCallerClass();
			checkAccess(caller, clazz, nul1, modifiers);
		}
	}
	if ((clazz.getModifiers() & Modifier.ENUM) != 0) {
		throw new IllegalArgumentException("Cannot reflectively create enum objects");
		
	}
	ConstructorAccessor ca = constructorAccessor;         // read volatile
	if (ca == null) {
		ca = acquireConstructorAccessor();
	}
	@Suppresslarnings("unchecked")
	T inst = (T) ca.newInstance(initargs];
	return inst;
}

其中关键的就是 T inst = (T) ca.newInstance(initargs);这一步,这里实现的话在BootstrapConstructorAccessorlmpl中,实现如下:

java 复制代码
public Object newInstance(Object[] args)
throws IllegalArgumentException,InvocationTargetException {
	try {
		return UnsafeFieldAccessorImpl.unsafe.allocateInstance(constructor.getDeclaringClass());
	} catch (InstantiationException e)  {
		throw new InvocationTargetException(e);
	}
}

可以看到,这里通过Java 的 Unsafe 机制来创建对象的,而不是通过调用构造函数。这意味着即使类的构造函数是私有的,反序列化仍然可以创建该类的实例,因为它不依赖于常规的构造过程。

So,到目前为止,也就可以解释,为什么序列化可以破坏单例?

答:序列化会通过Unsafe直接分配的方式来创建一个新的对象。

✅总结

在涉及到序列化的场景时,要格外注意他对单例的破坏。

✅如何避免单例被破坏

✅ 避免反射破坏单例

反射是调用默认的构造函数创建出来的,只需要我们改造下构造函数,使其在反射调用的时候识别出来对象是不是被创建过就行了:

java 复制代码
private Singleton() {
	if (singleton != null) {
		throw new RuntimeException("单例对象只能创建一次...");
	}
}

✅ 避免反序列化破坏单例

解决反序列化的破坏单例,只需要我们自定义反序列化的策略就行了,就是说我们不要让他走默认逻辑一直调用至Unsafe创建对象,而是我们干预他的这个过程,干预方式就是在Singleton类中定义 readResolve ,这样就可以解决该问题:

java 复制代码
package com.yangxiaoyuan;


import java.io.Serializable;

// 使用双重校验锁方式实现单例

public class Singleton implements Serializable {
	private volatile static Singleton singleton;
	private Singleton (){}
	public static Singleton getSingleton()  {
		if (singleton == null) {
			synchronized (Singleton.class)  {
				if (singleton == null) {
					singleton = new Singleton();
				}
			}
		}
		return singleton;
	}
	private Object readResolve() {
		return singleton;
	}
}

还是运行以下测试类

java 复制代码
package com.yangxiaoyuan;

import java.io.*;

public class SerializableDemo1 {
	//为了便于理解,忽略关闭流操作及删除文件操作。真正编码时千万不要忘记
	//Exception直接抛出

	public static void main(Stringl] args) throws IOException, ClassNotFoundException {
		//Write Obj to file
		ObjectOutputStream oos = new ObiectOutputStream(new File0utputstream("tempFile")):
		oos.writeObject(Singleton.getSingleton());
		//Read Obj from file
		File file = new File("tempFile");
		ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
		Singleton newInstance = (Singleton) ois.readObject();
		//判断是否是同一个对象
		System.out.println(newInstance == Singleton.getSingleton());
	}
}

//true

本次输出结果为true。具体原理,我们回过头继续分析code 3中的第二段代码:

java 复制代码
if (obj != null &&

handles.lookupException(passHandle) == null && desc.hasReadResolveMethod()) {
	Object rep = desc.invokeReadResolve(obj);
	if (unshared && rep.getClass().isArray()) {
		rep = cloneArray(rep);
	}
	if (rep != obj) {
		handles .setObject(passHandle, obj = rep);
	}
}

hasReadResolveMethod :如果实现了serializable 或者 externalizable接口的类中包含 readResolve 则返回true。

invokeReadResolve :通过反射的方式调用要被反序列化的类的readResolve方法。

所以,原理也就清楚了,只要在Singleton中定义readResolve方法,并在该方法中指定要返回的对象的生成策略,就可可以防止单例被破坏。

相关推荐
Monodye8 分钟前
【Java】网络编程:TCP_IP协议详解(IP协议数据报文及如何解决IPv4不够的状况)
java·网络·数据结构·算法·系统架构
一丝晨光14 分钟前
逻辑运算符
java·c++·python·kotlin·c#·c·逻辑运算符
元气代码鼠15 分钟前
C语言程序设计(进阶)
c语言·开发语言·算法
霍霍哈嗨27 分钟前
【QT基础】创建项目&项目代码解释
开发语言·qt
friklogff28 分钟前
【C#生态园】从图像到视觉:Emgu.CV、AForge.NET、OpenCvSharp 全面解析
开发语言·c#·.net
无名指的等待71238 分钟前
SpringBoot中使用ElasticSearch
java·spring boot·后端
Tatakai251 小时前
Mybatis Plus分页查询返回total为0问题
java·spring·bug·mybatis
武子康1 小时前
大数据-133 - ClickHouse 基础概述 全面了解
java·大数据·分布式·clickhouse·flink·spark
.生产的驴1 小时前
SpringBoot 消息队列RabbitMQ 消费者确认机制 失败重试机制
java·spring boot·分布式·后端·rabbitmq·java-rabbitmq
虚拟搬运工1 小时前
Python类及元类的创建流程
开发语言·chrome·python