前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >哪些情况下的单例对象可能会破坏?

哪些情况下的单例对象可能会破坏?

作者头像
Tom弹架构
发布2022-08-22 14:21:46
2780
发布2022-08-22 14:21:46
举报
文章被收录于专栏:Tom弹架构

昨天,有位小伙伴在评论区留言,希望我分享一些设计模式相关的面试题。设计模式本身是很抽象的,但是在很多面试中又经常被问到,很多小伙伴其实都能答得上,但是又不知道怎么样回答才能让面试官满意,往往越简单的知识越能够体现出核心竞争力。

今天,我给大家分享一个简单又不简单的单例模式,希望能够帮助到大家。先来看单例模式的定义。

1、单例模式的定义

关于单例模式的定义,官方原文是这样描述的:

Ensure a class has only one instance,and provide a global point of access to it.

大致意思是,确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点。

单例模式的写法相信只要是程序员应该都会,也很非常简单,这里我就不一一列举了。今天,我要重点要给大家分析的是,在Java中,哪些单例对象是最有可能被破坏的。

2、单例被破坏的五个场景

我把可能出现单例被破坏的情况,一共归纳为五种,分别为多线程破坏单例、指令重排破坏单例、克隆破坏单例、反序列化破坏单例、反射破坏单例。

下面我详细分析一下每种情况并给出解决方案:

第一种:多线程破坏单例

在多线程环境下,线程的时间片是由CPU自由分配的,具有随机性,而单例对象作为共享资源可能会同时被多个线程同时操作,从而导致同时创建多个对象。当然,这种情况只出现在懒汉式单例中。如果是饿汉式单例,在线程启动前就被初始化了,不存在线程再创建对象的情况。

如果懒汉式单例出现多线程破坏的情况,我给出以下两种解决方案:

1、改为DCL双重检查锁的写法。

2、使用静态内部类的写法,性能更高。

第二种:指令重排破坏单例

指令重排也可能导致懒汉式单例被破坏。来看这样一句代码:

instance = new Singleton();

看似简单的一段赋值语句:instance = new Singleton();

其实JVM内部已经被转换为多条执行指令:

memory = allocate(); 分配对象的内存空间指令

ctorInstance(memory); 初始化对象

instance = memory; 将已分配存地址赋值给对象引用

1、分配对象的内存空间指令,调用allocate()方法分配内存。

2、调用ctorInstance()方法初始化对象

3、将已分配存地址赋值给对象引用

但是经过重排序后,执行顺序可能是这样的:

memory = allocate(); 分配对象的内存空间指令

instance = memory; 将已分配存地址赋值给对象引用

ctorInstance(memory); 初始化对象

1、分配对象的内存空间指令

2、设置instance指向刚分配的内存地址

3、初始化对象

我们可以看到指令重排之后,instance指向分配好的内存放在了前面,而这段内存的初始化的指令被排在了后面,在线程 T1 初始化完成这段内存之前,线程T2 虽然进不去同步代码块,但是在同步代码块之前的判断就会发现 instance 不为空,此时线程T2 获得 instance 对象,如果直接使用就可能发生错误。

如果出现这种情况,我该如何解决呢?只需要在成员变量前加volatile,保证所有线程的可见性就可以了。

private static volatile Singleton instance = null;

第三种:克隆破坏单例

在Java中,所有的类就继承自Object,也就是说所有的类都实现了clone()方法。如果是深clone(),每次都会重新创建新的实例。那如果我们定义的是单例对象,岂不是也可调用clone()方法来反复创建新的实例呢?确实,这种情况是有可能发生的。为了避免发生这样结果,我们可以在单例对象中重写clone() 方法,将单例自身的引用作为返回值。这样,就能避免这种情况发生。

第四种:反序列化破坏单例

我们将Java对象序列化以后,对象通常会被持久化到磁盘或者数据库。如果我们要再次加载到内存,就需要将持久化的内容反序列化成Java对象。反序列化是基于字节码来操作的,我们要序列化以前的内容进行反序列化到内存,就需要重新分配内存,也就是说,要重新创建对象。那如果要反序列化的对象恰恰是单例对象,我们该怎么办呢?

我告诉大家一种解决方案,在反序列的过程中,Java API会调用readResolve()方法,可以通过获取readResolve()方法的返回值覆盖反序列化创建的对象。

因此,只需要重写readResolve()方法,将返回值设置为已经存在的单例对象,就可以保证反序列化以后的对象是同一个了。之后再将反序列化后的对象中的值,克隆到单例对象中。

第五种:反射破坏单例

以上讲的所有单例情况都有可能被反射破坏。因为Java中的反射机制是可以拿到对象的私有的构造方法,也就是说,反射可以任意调用私有构造方法创建单例对象。当然,没有人会故意这样做,但是如果出现意外的情况,该如何处理呢?我推荐大家两种解决方案,

第一种方案是在所有的构造方法中第一行代码进行判断,检查单例对象是否已经被创建,如果已经被创建,则抛出异常。这样,构造方法将会被终止调用,也就无法创建新的实例。

第二种方案,将单例的实现方式改为枚举式单例,因为在JDK源码层面规定了,不允许反射访问枚举。

3、总结

最后总结一下:

1、在所有单例写法中,如果程序不是太复杂,单例对象又不多,推荐使用饿汉式单例。

2、但如果经常发生多线程并发情况下,推荐使用静态内部类和枚举式单例,我的《设计模式就该这样学》这本书中,也推荐这样的写法。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-05-11,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Tom弹架构 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1、单例模式的定义
  • 2、单例被破坏的五个场景
  • 3、总结
相关产品与服务
文件存储
文件存储(Cloud File Storage,CFS)为您提供安全可靠、可扩展的共享文件存储服务。文件存储可与腾讯云服务器、容器服务、批量计算等服务搭配使用,为多个计算节点提供容量和性能可弹性扩展的高性能共享存储。腾讯云文件存储的管理界面简单、易使用,可实现对现有应用的无缝集成;按实际用量付费,为您节约成本,简化 IT 运维工作。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档