当前位置: 首页 > news >正文

如何实现单例模式及不同实现方法分析-设计模式

这是 一道面试常考题:(经常会在面试中让手写一下)

什么是单例模式

【问什么是单例模式时,不要答非所问,给出单例模式有两种类型之类的回答,要围绕单例模式的定义去展开。】

单例模式是指在内存中只会创建且仅创建一次对象的设计模式。在程序中多次使用同一个对象且作用相同时,为了防止频繁地创建对象使得内存飙升,单例模式可以让程序仅在内存中创建一个对象,让所有需要调用的地方都共享这一单例对象。

单例模式(Singleton Pattern)是一种常用的设计模式,保证一个类在内存中只有一个实例,并提供一个全局访问点。单例模式通常用于管理共享资源、控制全局状态或提供全局服务。

单例模式有两种类型:

  • 懒汉式:在真正需要使用对象时才去创建该单例类对象
  • 饿汉式:在类加载时已经创建好该单例对象,等待被程序使用

单例模式实现方法:

一、饿汉式单例:在类初始化时就已经自行实例化了
      public class Singleton {//私有静态成员变量private static Singleton instance = new Singleton();//私有构造方法private Singleton(){}//公有静态访问方法public static Singleton getInstance(){return instance;}}

注意上面的代码在第2行已经实例化好了一个Singleton对象在内存中,不会有多个Singleton对象实例存在;类在加载时会在堆内存中创建一个Singleton对象当类被卸载时,Singleton对象也随之消亡了。

当然可以改为静态方块来执行实例化语句:

              private static Singleton instance = null;
              static{
                instance = new Singleton();
            }

二、懒汉式单例:在第一次调用实例的时候才实例化

如果两个线程同时判断singleton为空,那么它们都会去实例化一个Singleton对象,这就变成双例了,就不是单例了,所以可以在方法上加锁或类 对象上  加锁,

      public class Singleton {//私有静态成员变量private static Singleton instance;//私有构造方法private Singleton(){}//公有静态访问方法,在方法上加了一个synchronized关键字确保线程安全public static synchronized Singleton getInstance(){if(instance == null)instance = new Singleton();return instance;}}// 或者(在类对象上加锁)   public static Singleton getInstance() {synchronized(Singleton.class) {   if (singleton == null) {singleton = new Singleton();}}return singleton;}

这样就规避了两个线程同时创建Singleton对象的风险,但是引来另外一个问题:每次去获取对象都需要先获取锁,并发性能非常地差,极端情况下,可能会出现卡顿现象。

接下来要做的就是优化性能,目标是:如果没有实例化对象则加锁创建,如果已经实例化了,则不需要加锁,直接获取实例

所以直接在方法上加锁的方式就被废掉了,因为这种方式无论如何都需要先获取锁

接下来有下面的DCL

三、双重检测锁模式的懒汉式单例:(线程安全效率高)

        又叫DCL懒汉式 (Double Check Lock)

      public class Singleton {//私有静态成员变量,加上了volatile关键字确保可见性private volatile static Singleton instance = null;//私有构造方法private Singleton(){}//公有静态访问方法public static  Singleton getInstance(){if(instance == null){ //线程A和线程B同时看到singleton = null,如果不为null,则直接返回singletonsynchronized (Singleton.class){ //线程A或线程B获得该锁进行初始化;获取锁这里利用到volatile关键字的可见性,再次判断空if(instance == null) //其中一个线程进入该分支,另外一个线程则不会进入该分支,此时instance真的为空,才去创建实例instance = new Singleton();}}return instance;}}

 

注意:synchronized 解决并发问题,但是因为lazyMan = new LazyMan();不是原子性操作(可以分割,见代码注释),可能发生指令重排序的问题,通过volatil来解决

  • Java 语言提供了 volatile和 synchronized 两个关键字来保证线程之间操作的有序性,volatile 是因为其本身包含“禁止指令重排序”的语义,synchronized 是由“一个变量在同一个时刻只允许一条线程对其进行 lock 操作”这条规则获得的,此规则决定了持有同一个对象锁的两个同步块只能串行执行。
  • 原子性就是指该操作是不可再分的。不论是多核还是单核,具有原子性的量,同一时刻只能有一个线程来对它进行操作。简而言之,在整个操作过程中不会被线程调度器中断的操作,都可认为是原子性。比如 a = 1;

 指令重排序是指:JVM在保证最终结果正确的情况下,可以不按照程序编码的顺序执行语句,尽可能提高程序的性能

  • 使用volatile关键字可以防止指令重排序,​其原理较为复杂,这篇博客不打算展开,可以这样理解:使用volatile关键字修饰的变量,可以保证其指令执行的顺序与程序指明的顺序一致,不会发生顺序变换,这样在多线程环境下就不会发生NPE异常了。
  • volatile还有第二个作用:使用volatile关键字修饰的变量,可以保证其内存可见性,即每一时刻线程读取到该变量的值都是内存中最新的那个值,线程每次操作该变量都需要先读取该变量。
四、破坏懒汉式单例与饿汉式单例

无论是完美的懒汉式还是饿汉式,终究敌不过反射和序列化,它们俩都可以把单例对象破坏掉(产生多个对象)。

1.演示利用反射破坏单例模式

public static void main(String[] args) {// 获取类的显式构造器Constructor<Singleton> construct = Singleton.class.getDeclaredConstructor();// 可访问私有构造器construct.setAccessible(true); // 利用反射构造新对象Singleton obj1 = construct.newInstance(); // 通过正常方式获取单例对象Singleton obj2 = Singleton.getInstance(); System.out.println(obj1 == obj2); // false
}

上述的代码一针见血了:利用反射,强制访问类的私有构造器,去创建另一个对象

 2.利用序列化与反序列化破坏单例模式

public static void main(String[] args) {// 创建输出流ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("Singleton.file"));// 将单例对象写到文件中oos.writeObject(Singleton.getInstance());// 从文件中读取单例对象File file = new File("Singleton.file");ObjectInputStream ois =  new ObjectInputStream(new FileInputStream(file));Singleton newInstance = (Singleton) ois.readObject();// 判断是否是同一个对象System.out.println(newInstance == Singleton.getInstance()); // false
}

 两个对象地址不相等的原因是:readObject() 方法读入对象时,它必定会返回一个新的对象实例,必然指向新的内存地址。

五、枚举实现

至此我们已经掌握了懒汉式与饿汉式的常见写法了,在《大话设计模式》中的单例模式章节也止步于此。但是,追求极致的我们,怎么能够止步于此,在《Effective Java》书中,给出了终极解决方法,话不多说,学完下面,真的不虚面试官考你了。

在 JDK1.5 后,使用 Java 语言实现单例模式的方式又多了一种:枚举

我们先来看看枚举如何实现单例模式的,如下代码:

public enum Singleton {INSTANCE;public void doSomething() {System.out.println("这是枚举类型的单例模式!");}
}

需要思考:使用枚举实现单例模式的优势在哪里?

我们从最直观的地方入手,第一眼看到这几行代码,就会感觉到“少”,没错,就是少,虽然这优势有些牵强,但写的代码越少,越不容易出错。

优势1:代码对比饿汉式与懒汉式来说,更加地简洁

其次,既然是实现单例模式,那这种写法必定满足单例模式的要求,而且使用枚举实现时,没有做任何额外的处理。

优势2:它不需要做任何额外的操作去保证对象单一性与线程安全性

我写了一段测试代码放在下面,这一段代码可以证明程序启动时仅会创建一个 Singleton 对象,且是线程安全的。

我们可以简单地理解枚举实现单例的过程:在程序启动时,会调用Singleton的空参构造器,实例化好一个Singleton对象赋给INSTANCE,之后再也不会实例化

public enum Singleton {INSTANCE;Singleton() { System.out.println("枚举创建对象了"); }public static void main(String[] args) { /* test(); */ }public void test() {Singleton t1 = Singleton.INSTANCE;Singleton t2 = Singleton.INSTANCE;System.out.print("t1和t2的地址是否相同:" + t1 == t2);}
}
// 枚举创建对象了
// t1和t2的地址是否相同:true

除了优势1和优势2,还有最后一个优势让枚举实现单例模式在目前看来已经是“无懈可击”了。

优势3:使用枚举可以防止调用者使用反射序列化与反序列化机制强制生成多个单例对象,破坏单例模式。

防破坏的原理如下:

(1)防反射

枚举类默认继承了 Enum 类,在利用反射调用 newInstance() 时,会判断该类是否是一个枚举类,如果是,则抛出异常。

(2)防止反序列化创建多个枚举对象

在读入Singleton对象时,每个枚举类型和枚举名字都是唯一的,所以在序列化时,仅仅只是对枚举的类型和变量名输出到文件中,在读入文件反序列化成对象时,利用 Enum 类的 valueOf(String name) 方法根据变量的名字查找对应的枚举对象。

所以,在序列化和反序列化的过程中,只是写出和读入了枚举类型和名字,没有任何关于对象的操作。

小总结:

(1)Enum 类内部使用Enum 类型判定防止通过反射创建多个对象

(2)Enum 类通过写出(读入)对象类型和枚举名字将对象序列化(反序列化),通过 valueOf() 方法匹配枚举名找到内存中的唯一的对象实例,防止通过反序列化构造多个对象

(3)枚举类不需要关注线程安全、破坏单例和性能问题,因为其创建对象的时机与饿汉式单例有异曲同工之妙。

总结:

(1)单例模式常见的写法有两种:懒汉式、饿汉式

(2)饿汉式:在类加载时已经创建好该单例对象,在获取单例对象时直接返回对象即可,不会存在并发安全和性能问题。

(3)懒汉式:在需要用到对象时才实例化对象,正确的实现方式是:Double Check + Lock,解决了并发安全和性能低下问题

(4)在开发中如果对内存要求非常高,那么使用懒汉式写法,可以在特定时候才创建该对象;

(5)如果对内存要求不高使用饿汉式写法,因为简单不易出错,且没有任何并发安全和性能问题

(6)为了防止多线程环境下,因为指令重排序导致变量报NPE,需要在单例对象上添加volatile关键字防止指令重排序

(7)最优雅的实现方式是使用枚举,其代码精简,没有线程安全问题,且 Enum 类内部防止反射和反序列化时破坏单例。

相关文章:

如何实现单例模式及不同实现方法分析-设计模式

这是 一道面试常考题&#xff1a;&#xff08;经常会在面试中让手写一下&#xff09; 什么是单例模式 【问什么是单例模式时&#xff0c;不要答非所问&#xff0c;给出单例模式有两种类型之类的回答&#xff0c;要围绕单例模式的定义去展开。】 单例模式是指在内存中只会创建…...

wampserver安装与汉化

wampserver安装与汉化 文章目录 wampserver安装与汉化一、安装二、汉化1.升级软件并安装补丁 介绍&#xff1a; WampServer是一款由法国人开发的Apache Web服务器、PHP解释器以及MySQL数据库的整合软件包。免去了开发人员将时间花费在繁琐的配置环境过程&#xff0c;从而腾出更…...

解决MyBatis的N+1问题

解决MyBatis的N1问题 N1问题通常出现在一对多关联查询中。当我们查询主表数据&#xff08;如订单&#xff09;并希望获取关联的从表数据&#xff08;如订单的商品&#xff09;时&#xff0c;如果每获取一条主表记录都要执行一次从表查询&#xff0c;就会产生N1次查询的问题。假…...

12-学生们参加各科测试的次数(高频 SQL 50 题基础版)

12-学生们参加各科测试的次数 -- 学生表中&#xff0c;id是唯一的&#xff0c;将他作为主表 -- CROSS JOIN产生了一个结果集&#xff0c;该结果集是两个关联表的行的乘积 -- 2行表,与3行表使用cross join,得到2*36行数据 select st.student_id, st.student_name,su.subject_na…...

2024网络与信息安全管理员职工职业技能竞赛re0220164094

main部分&#xff0c;就是要逆这部分shellcode&#xff0c;程序把data段里面的东西复制到bss段去执行&#xff0c;期间包含解码操作。 v19 0;puts("Please input your flag: ");__isoc99_scanf("%s", s);if ( strlen(s) ! 38 ){puts("Wrong length!&…...

Elasticsearch--easy-ES框架使用,轻松操作查询Elasticsearch,简化开发

Easy-Es&#xff08;简称EE&#xff09;是一款基于ElasticSearch(简称Es)官方提供的RestHighLevelClient打造的ORM开发框架&#xff0c;在 RestHighLevelClient 的基础上,只做增强不做改变&#xff0c;为简化开发、提高效率而生,您如果有用过Mybatis-Plus(简称MP),那么您基本可…...

【教程】如何实现WordPress网站降级(用于解决插件和主题问题)

在最新可用版本上运行WordPress安装、插件和主题是使用该平台的关键最佳实践。还建议使用最新版本的PHP。但是,在某些情况下,这是不谨慎或不可能的。 如果您发现自己处于这种情况,您可能需要撤消更新并降级您的WordPress网站(或其中的一部分)。幸运的是,有一些方法可用于…...

思维导图-vb.net开发带进度条的复制文件夹功能c#复制文件夹

你们谁写代码会用流程图来做计划&#xff0c;或者写项目总结报告&#xff1f; .net带进度条复制文件夹 方案 列出所有子文件夹&#xff0c;再创建&#xff0c;复制文件 大文件可以单独做进度条 缺点&#xff1a;设计会更复杂 直接…...

Linux文本处理三剑客之awk命令

官方文档&#xff1a;https://www.gnu.org/software/gawk/manual/gawk.html 什么是awk&#xff1f; Awk是一种文本处理工具&#xff0c;它的名字是由其三位创始人&#xff08;Aho、Weinberger和Kernighan&#xff09;的姓氏首字母组成的。Awk的设计初衷是用于处理结构化文本数…...

公差和配合

配合的选择&#xff1a; 配合特性以及基本偏差的应用&#xff1a; 常用优先配合特性及选用举例 为什么一般情况下选用基孔制而不用基轴制&#xff1a; 优先采用基孔制的原因主要包括工艺性、经济性和标准化&#xff1a; 工艺性。加工孔比加工轴更难&#xff0c;因为孔…...

AI大模型应用开发实践:5.快速入门 Assistants API

快速入门 Assistants API Assistants API 允许您在自己的应用程序中构建人工智能助手。一个助手有其指令,并可以利用模型、工具和知识来回应用户查询。 Assistants API 目前支持三种类型的工具: 代码解释器 Code Interpreter检索 Retrieval函数调用 Function calling使用 P…...

stack和queue的模拟实现

文章目录 如何实现&#xff1f;实现stack实现queue总结 如何实现&#xff1f; 首先我们看看官网上的stack&#xff0c;官网上的stack是用deque作为模版的缺省值去实现的&#xff0c;deque是什么&#xff1f; deque其实就是双端队列&#xff0c;双端队列&#xff0c;顾名思义&am…...

你的手机是如何控制你的手表之广播篇

前言 要让手机能够控制手表&#xff0c;第一步当然要让手机能够“看见”手表&#xff0c;人类作为上帝视角&#xff0c;我们是能够通过眼睛直接看见手机和手表的&#xff0c;但要让手机“看见”手表&#xff0c;就需要一端把自己的信息通过电磁波的形式发往空中&#xff0c;另…...

深入理解并发之LongAdder、DoubleAdder的实现原理

深入理解LongAdder、DoubleAdder的实现原理 本文主要通过LongAdder和DoubleAdder的源码&#xff0c;讲述一下其实现原理。通过LongAdder和DoubleAdder的源码可知。两者都是继承了Striped64的类。下面我们将通过源码的形式讲述一下这三个类都做了哪些事情。 1: Striped64 ​ …...

virtuoso原理图无法编辑

(SCH-1217): Could not open "XX schematic" for edit. (because it is locked by user XX on XX) Would you like to open it for read? 解决方法&#xff1a; 到工程目录的schematic文件夹下找到sch.oa.cdslck.RHEL30.XXX-eda.21423和sch.oa.cdslck全部删掉即可正…...

Kotlin协程中的作用域 `GlobalScope`、`lifecycleScope` 和 `viewModelScope`

Kotlin协程中的作用域 Kotlin协程提供了多种作用域来管理协程的生命周期&#xff0c;其中最常见的是 GlobalScope、lifecycleScope 和 viewModelScope。 1. GlobalScope GlobalScope 是一个全局作用域&#xff0c;不受任何其他生命周期的限制。这意味着在 GlobalScope 中启动…...

leetcode739 每日温度

题目 给定一个整数数组 temperatures &#xff0c;表示每天的温度&#xff0c;返回一个数组 answer &#xff0c;其中 answer[i] 是指对于第 i 天&#xff0c;下一个更高温度出现在几天后。如果气温在这之后都不会升高&#xff0c;请在该位置用 0 来代替。 示例 输入: tempe…...

【软件测试】自动化测试如何管理测试数据

前言 在之前的自动化测试框架相关文章中&#xff0c;无论是接口自动化还是UI自动化&#xff0c;都谈及data模块和config模块&#xff0c;也就是测试数据和配置文件。 随着自动化用例的不断增加&#xff0c;需要维护的测试数据也会越来越多&#xff0c;维护成本越来越高&#…...

Llama 3-V: 比GPT4-V小100倍的SOTA

大模型技术论文不断&#xff0c;每个月总会新增上千篇。本专栏精选论文重点解读&#xff0c;主题还是围绕着行业实践和工程量产。若在某个环节出现卡点&#xff0c;可以回到大模型必备腔调重新阅读。而最新科技&#xff08;Mamba&#xff0c;xLSTM,KAN&#xff09;则提供了大模…...

Anaconda安装配置

Anaconda下载&#xff1a; 网盘下载&#xff1a;https://pan.quark.cn/s/c5663477ccef 配置环境变量&#xff1a; 以管理员身份运行命令提示符 setx /M PATH "%PATH%;C:\ProgramData\anaconda3;C:\ProgramData\anaconda3\Scripts;C:\ProgramData\anaconda3\Library\bi…...

Python爬虫(二):爬虫完整流程

爬虫完整流程详解&#xff08;7大核心步骤实战技巧&#xff09; 一、爬虫完整工作流程 以下是爬虫开发的完整流程&#xff0c;我将结合具体技术点和实战经验展开说明&#xff1a; 1. 目标分析与前期准备 网站技术分析&#xff1a; 使用浏览器开发者工具&#xff08;F12&…...

学习STC51单片机31(芯片为STC89C52RCRC)OLED显示屏1

每日一言 生活的美好&#xff0c;总是藏在那些你咬牙坚持的日子里。 硬件&#xff1a;OLED 以后要用到OLED的时候找到这个文件 OLED的设备地址 SSD1306"SSD" 是品牌缩写&#xff0c;"1306" 是产品编号。 驱动 OLED 屏幕的 IIC 总线数据传输格式 示意图 …...

Module Federation 和 Native Federation 的比较

前言 Module Federation 是 Webpack 5 引入的微前端架构方案&#xff0c;允许不同独立构建的应用在运行时动态共享模块。 Native Federation 是 Angular 官方基于 Module Federation 理念实现的专为 Angular 优化的微前端方案。 概念解析 Module Federation (模块联邦) Modul…...

DBAPI如何优雅的获取单条数据

API如何优雅的获取单条数据 案例一 对于查询类API&#xff0c;查询的是单条数据&#xff0c;比如根据主键ID查询用户信息&#xff0c;sql如下&#xff1a; select id, name, age from user where id #{id}API默认返回的数据格式是多条的&#xff0c;如下&#xff1a; {&qu…...

k8s业务程序联调工具-KtConnect

概述 原理 工具作用是建立了一个从本地到集群的单向VPN&#xff0c;根据VPN原理&#xff0c;打通两个内网必然需要借助一个公共中继节点&#xff0c;ktconnect工具巧妙的利用k8s原生的portforward能力&#xff0c;简化了建立连接的过程&#xff0c;apiserver间接起到了中继节…...

日常一水C

多态 言简意赅&#xff1a;就是一个对象面对同一事件时做出的不同反应 而之前的继承中说过&#xff0c;当子类和父类的函数名相同时&#xff0c;会隐藏父类的同名函数转而调用子类的同名函数&#xff0c;如果要调用父类的同名函数&#xff0c;那么就需要对父类进行引用&#…...

微服务通信安全:深入解析mTLS的原理与实践

&#x1f525;「炎码工坊」技术弹药已装填&#xff01; 点击关注 → 解锁工业级干货【工具实测|项目避坑|源码燃烧指南】 一、引言&#xff1a;微服务时代的通信安全挑战 随着云原生和微服务架构的普及&#xff0c;服务间的通信安全成为系统设计的核心议题。传统的单体架构中&…...

macOS 终端智能代理检测

&#x1f9e0; 终端智能代理检测&#xff1a;自动判断是否需要设置代理访问 GitHub 在开发中&#xff0c;使用 GitHub 是非常常见的需求。但有时候我们会发现某些命令失败、插件无法更新&#xff0c;例如&#xff1a; fatal: unable to access https://github.com/ohmyzsh/oh…...

UE5 音效系统

一.音效管理 音乐一般都是WAV,创建一个背景音乐类SoudClass,一个音效类SoundClass。所有的音乐都分为这两个类。再创建一个总音乐类&#xff0c;将上述两个作为它的子类。 接着我们创建一个音乐混合类SoundMix&#xff0c;将上述三个类翻入其中&#xff0c;通过它管理每个音乐…...

未授权访问事件频发,我们应当如何应对?

在当下&#xff0c;数据已成为企业和组织的核心资产&#xff0c;是推动业务发展、决策制定以及创新的关键驱动力。然而&#xff0c;未授权访问这一隐匿的安全威胁&#xff0c;正如同高悬的达摩克利斯之剑&#xff0c;时刻威胁着数据的安全&#xff0c;一旦触发&#xff0c;便可…...