首页 > 其他分享 >手写一个单例模式然后问如何破坏这个单例模式

手写一个单例模式然后问如何破坏这个单例模式

时间:2024-07-10 17:12:08浏览次数:11  
标签:反射 对象 Enum 模式 破坏 单例 手写 序列化

手写一个单例模式然后问如何破坏这个单例模式

美团到店的原题,手写一个单例模式然后问如何破坏这个单例模式?

单例模式谁都会,懒汉、饿汉、双重校验锁、匿名内部类、Enum,倒背如流了都,那如何破坏单例呢?

以最简单的饿汉式写法为例:

所谓单例,就是保证一个类只有一个实例对象,那想要破坏单例模式,无非就是创建多个实例对象罢了

那单例模式的构造函数都是 private 的,我们没法直接通过 new 来构造对象,也就是说通过 new 这种方式去破坏单例的可能性是不存在的,得另寻他路。

除了 new,创建对象的方式还有 clone,反序列化,以及反射

要调用 clone 方法,那么必须实现 Cloneable 接口,但是单例模式是不能实现这个接口的,因此排除这种可能性。所以我们要讨论的其实就是如何通过反序列化反射对单例模式进行破坏

反序列化破坏单例

序列化是破坏单例模式的一大利器。相比于克隆,实现序列化在实际操作中更加不可避免,有些类,它就是一定要序列化。

下面我们来做个测试,在上面的单例模式中实现序列化接口,然后先通过 getInstance 拿到一个对象,对这个对象进行序列化再反序列化拿到一个对象,比较两个对象是否是同一个对象:

结果为 false,说明通过对 Singleton 的序列化再反序列化得到的对象是一个新的对象,这就破坏了 Singleton 的单例性。

反序列化是怎么创建一个新对象的?

我们可以点击 readObject 这个方法看看

核心是 readObject0,继续点进去:

根据传入参数类型的不同,调用了不同的方法进行反序列化,点进针对 Object 的 readOrdinaryObject 方法看看:

真相大白了,反序列化底层其实就是使用了反射帮我们创建了一个新的对象

如何阻止反序列化破坏单例?

现在我们在 Singleton 类中实现一个 readResolve 方法,该方法直接返回了这个单例对象:

重新执行下,发现结果为 true!也就是说 instance1 和 instance2 是同一个对象!

具体是什么原理,我们来看看刚才的 readOrdinaryObject 方法:

可以看到,在条件判断中 desc.hasReadResolveMethod() 会判断类是否有 readResolve() 方法,如果有的话会通过 desc.invokeReadResolve(obj) 去反射调用该方法,由于我们的 readResolve 方法直接返回了 instance,不会创建一个新对象,这样最终就保证了类实例对象的唯一性

所以,如果想要防止单例被反序列化破坏,就让单例类实现 readResolve() 方法

反射破坏单例

上面说到,反序列化底层其实就是通过反射来创建一个新对象的,我们直接来看反射是怎么破坏单例的:

执行结果当然是 false 了

如何阻止反射破坏单例?

反射是怎么创建新对象的?是通过类的构造函数来的

所以如果我们想要阻止反射破坏单例,我们就需要修改类的构造函数:

重新执行一遍我们的代码,不出所料抛异常了,这样便防止了单例被反射破坏:

不过这种构造函数判断的方法,只能阻止饿汉式的单例模式,没法阻止懒汉式的单例

我们可以来写个懒汉模式测试下:

执行下,发现结果仍然是抛异常:

什么情况?

别急,我们把 instance1 和 instance2 的构建顺序调换下:

再执行,结果就是 false 了!!!

这是因为懒汉式的对象只有调用的时候才被创建,我们先调用反射通过私有构造函数来创建对象,这样就越过了 instance != null 的判断,不会抛异常,再通过 getInstance 创建对象,这两个对象就不是同一个对象了,即单例模式被破坏了。

总结下,如果今后需要自己手动实现一个单例的话,可以选择【构造函数判断】+【实现 readResolve() 方法】的方式
来防止单例被破坏

优雅的单例实现:Enum

那如果我不想在构造函数里面做判断,也不想写 readResolve() 方法,我就想安安静静写个单例,有没有更简单更优雅的方法?

答案是有的!可以选择使用 Enum 枚举来实现单例模式

用反射来测试下,结果是直接抛异常了 java.lang.NoSuchMethodException

简单来说就是因为 singletonClass.getDeclaredConstructor() 没有找到 Singleton 的无参构造器,这是为啥?

主要是因为,一旦一个类声明为枚举,实际上就是继承了 java.lang.Enum,来看看 Enum 类源码:

Enum 有两个参数 nameordial 两个属性,我们自己写的单例类继承了父类 Enum 的构造函数,所以在上述的 getDecalredConstructor 才会找不到无参构造器,那么是不是我们去调用父类的构造器就可以了呢?我们来测试一下:

哦吼,运行后直接抛 IllegalArgumentException 异常了

无法通过反射创建 Enum 对象!!!我们点进去报错的 22 行即 constructor.newInstance 一探究竟:

简单来说就是反射在通过 newInstance 创建对象时,会检查该类是否被 ENUM 修饰,如果是则直接抛出异常,反射失败,所以枚举是不怕反射暴力破解构造器的


上面说枚举是可以阻止反射通过暴力破解构造函数来破坏单例的,再来看枚举是如何阻止反序列化破坏单例的。

事实上,枚举对象的序列化、反序列化有自己的一套机制:

  • 序列化的时候,仅仅是将枚举对象的 name 属性输出到结果中
  • 反序列化的时候则是通过 java.lang.Enum 的 valueOf 方法 来根据 name 查找枚举对象

来看看 Enum.valueOf 方法:

继续看 getEnumConstantsShared() 源码:

水落石出啦,仍然是通过反射做的,先获取枚举类的 values() 方法,再得到所有枚举对象。

简单总结下:

  • 每个枚举对象都有一个唯一的 name 属性
  • 序列化只是将 name 属性序列化
  • 在反序列化的时候,通过一个 Map(key,value) 存储 name 和与之对应的对象之间的映射,然后通过 name 就可以直接获得原来的 Enum 对象,而不会创建一个新对象!也就是说反序列化 Enum 类对象拿到的仍然是原来的对象,这样就使得 Enum 类型实现了单例模式下的序列化安全

标签:反射,对象,Enum,模式,破坏,单例,手写,序列化
From: https://www.cnblogs.com/ywbmaster/p/18294498

相关文章

  • Linux (10) 配置HAProxy,实现负载均衡器的主备模式
    《WindowsAzurePlatform系列文章目录》 最近有1个客户需求,在这里记录一下。客户提出需要使用Azure负载均衡器(四层负载均衡器),实现主备模式。场景是负载均衡器后有2台虚拟机-平时100%的流量都发送到第一台虚拟机-如果第一台虚拟机发生......
  • 设计模式使用场景实现示例及优缺点(创建型模式——单例模式、建造者模式、原型模式)
    创建型模式单例模式(SingletonPattern)单例模式(SingletonPattern)在Java中的使用场景与在其他编程语言中类似,其主要目的是确保一个类只有一个实例,并提供一个全局的访问点。以下是单例模式的一些常见使用场景及详细讲解:使用场景控制资源的使用:数据库连接池:数据库连接是......
  • 设计模式使用场景实现示例及优缺点(结构型模式——代理模式、外观模式)
    结构型模式代理模式(ProxyPattern)代理模式(ProxyPattern)是一种结构型设计模式,它通过引入一个代理对象来控制对另一个对象的访问。这个代理对象可以为被代理的对象提供额外的功能,例如访问控制、延迟初始化、日志记录、或网络访问等。适用场景远程代理:为一个对象在不同......
  • Xubuntu24.04之设置高性能模式两种方式(二百六十一)
    简介:CSDN博客专家,专注Android/Linux系统,分享多mic语音方案、音视频、编解码等技术,与大家一起成长!优质专栏:Audio工程师进阶系列【原创干货持续更新中……】......
  • go并发模式 o-channel
    packagemainimport("fmt""time")funcmain(){varorfunc(channels...<-chaninterface{})<-chaninterface{}or=func(channels...<-chaninterface{})<-chaninterface{}{switchlen(channels)......
  • go并发模式 or-do-channel + bridge
    packagemainimport("context""fmt")//orDonefuncorDone(ctxcontext.Context,value<-chanint)<-chanint{ordoneStream:=make(chanint)gofunc(){deferclose(ordoneStream)for{......
  • go并发模式 tee-channel
    packagemainimport("context""fmt""time")functeeChannel(ctxcontext.Context,value<-chanint)(<-chanint,<-chanint){ch1:=make(chanint)ch2:=make(chanint)gofunc(){......
  • 服务器ubuntu 20.04关闭自动休眠模式
    1.查看是否开启休眠模式systemctlstatussleep.target发现系统提示: 2.执行关闭休眠功能命令sudosystemctlmasksleep.targetsuspend.targethibernate.targethybrid-sleep.target系统返回: 3.观察系统休眠状态,如下:systemctlstatussleep.target可以看到自......
  • go并发模式 pipeline
    packagemainimport("fmt""math/rand")funcmain(){pFn:=func(done<-chaninterface{},fnfunc()int)<-chanint{valueStream:=make(chanint)gofunc(){deferclose(valueStream)......
  • go并发模式 错误处理
    packagemainimport("fmt""net/http")typeResultsstruct{ErrorerrorResponse*http.Response}funcmain(){checkStatus:=func(done<-chaninterface{},urls...string)<-chanResults{re......