单例模式(Singleton)

单例模式保证一个类仅有一个实例,并提供一个全局访问点来访问它,这个类称为单例类。可见,在实现单例模式时,除了保证一个类只能创建一个实例外,还需提供一个全局访问点。

Singleton is a creational design pattern that lets you ensure that a class has only one instance, 
while providing a global access point to this instance.  

为提供一个全局访问点,可以使用全局变量,但全局变量无法禁止用户实例化多个对象。为此,可以让类自身负责保存它的唯一实例。这个类可以保证没有其他实例可以被创建,并且它可以提供一个访问实例的方法。
综上,单例模式的要点有三个:一是某个类只能有一个实例;二是这个类必须自行创建这个实例;三是这个类必须自行向整个系统提供这个实例。

结构设计

单例模式只有一个角色:
Singleton,单例类,用来保证实例唯一并提供一个全局访问点。为实现访问点全局唯一,可以定义一个静态字段,同时为了封装对该静态字段的访问,可以定义一个静态方法。为了保证实例唯一,这个类还需要在内部保证实例的唯一。基于以上思考,单例模式的类图表示如下:
请添加图片描述

伪代码实现

接下来将使用代码介绍下单例模式的实现。单例模式的实现方式有很多种,主要的实现方式有以下五种:饿汉方式、懒汉方式、线程安全实现方式、双重校验方式、惰性加载方式。

(1) 饿汉方式

饿汉方式就是在类加载的时候就创建实例,因为是在类加载的时候创建实例,所以实例必唯一。由于在类加载的时候创建实例,如果实例较复杂,会延长类加载的时间。

// 1. 定义单例类,提供全局唯一访问点,保证实例唯一
public class HungrySingleton {// (1) 声明并实例化静态私有成员变量(在类加载的时候创建静态实例)private static final HungrySingleton instance = new HungrySingleton();// (2) 私有构造方法private HungrySingleton() {}// (3) 定义静态方法,提供全局唯一访问点public static HungrySingleton getInstance() {return instance;}public void foo() {System.out.println("---------do some thing in a HungrySingleton instance---------");}
}
// 2. 客户端调用
public class HungrySingletonClient {public void test() {// (1) 获取实例HungrySingleton singleton = HungrySingleton.getInstance();// (2) 调用实例方法singleton.foo();}
}

(2) 懒汉方式

懒汉方式就是在调用实例获取(如getInstance())接口时,再创建实例,这种方式可避免在加载类的时候就初始化实例。

// 1. 定义单例类,提供全局唯一访问点,保证实例唯一
public class LazySingleton {// (1) 声明静态私有成员变量private static LazySingleton instance;// (2) 私有构造方法private LazySingleton() {}// (3) 定义静态方法,提供全局唯一访问点public static LazySingleton getInstance() {// 将实例的创建延迟到第一次获取实例if(instance == null) {instance = new LazySingleton();}return instance;}public void foo() {System.out.println("---------do some thing in a LazySingleton instance---------");}
}
// 2. 客户端调用
public class LazySingletonClient {public void test() {// (1) 获取实例LazySingleton instance = LazySingleton.getInstance();// (2) 调用实例方法instance.foo();}
}

需要说明的是,对多线程语言来说(如java语言),懒汉方式会带来线程不安全问题。如果在实例前执行判空处理时,至少两个线程同时进入这行代码,则会创建多个实例。
所以,对于多线程语言来说,为了保证代码的正确性,还需在实例化的时候,保证线程安全。

(3) 线程安全实现方式

为保证线程安全,可以在实例判空前,进行线程同步处理,如添加互斥锁。

// 1. 定义单例类,提供全局唯一访问点,保证实例唯一
public class ThreadSafeSingleton {// (1) 声明静态私有成员变量private static ThreadSafeSingleton instance;// (2) 私有构造方法private ThreadSafeSingleton() {}// (3) 定义静态方法,提供全局唯一访问点public static ThreadSafeSingleton getInstance() {// 使用synchronized方法,保证线程安全synchronized (ThreadSafeSingleton.class) {if (Objects.isNull(instance)) {instance = new ThreadSafeSingleton();}return instance;}}public void foo() {System.out.println("---------do some thing in a ThreadSafeSingleton instance---------");}
}
// 2. 客户端调用
public class ThreadSafeSingletonClient {public void test() {// (1) 获取实例ThreadSafeSingleton instance = ThreadSafeSingleton.getInstance();// (2) 调用实例方法instance.foo();}
}

但是这种方式,会因线程同步而带来性能问题。因为大多数场景下,是不存在并发访问。

(4) 双重校验方式

为避免每次创建实例时加锁带来的性能问题,引入双重校验方式,即在加锁前额外进行实例判空校验,这样就可保证非并发场景下仅在第一次实例化时,去加锁并创建实例。

// 1. 定义单例类,提供全局唯一访问点,保证实例唯一
public class DoubleCheckSingleton {// (1) 声明静态私有成员变量private static volatile DoubleCheckSingleton instance;// (2) 私有构造方法private DoubleCheckSingleton() {}// (3) 定义静态方法,提供全局唯一访问点public static DoubleCheckSingleton getInstance() {// 在加锁之前,先执行判空检验,提高性能if (Objects.isNull(instance)) {// 使用synchronized方法,保证线程安全synchronized (DoubleCheckSingleton.class) {if (Objects.isNull(instance)) {instance = new DoubleCheckSingleton();}}}return instance;}public void foo() {System.out.println("---------do some thing in a DoubleCheckSingleton instance---------");}
}
// 2. 客户端调用
public class DoubleCheckSingletonClient {public void test() {// (1) 获取实例DoubleCheckSingleton instance = DoubleCheckSingleton.getInstance();// (2) 调用实例方法instance.foo();}
}

注意,使用双重校验方式时,需明确语言是否支持指令重排序。以Java语言为例,实例化一个对象的过程是非原子的。具体来说,可以分为以下三步:(1) 分配对象内存空间;(2)将对象信息写入上述内存空间;(3) 创建对上述内存空间的引用。其中(2)和(3)的顺序不要求固定(无先后顺序),所以存在实例以分配内存空间但还未初始化的情况。如果此时存在并发线程使用了该未初始化的对象,则会导致代码异常。为避免指令重排序,Java语言中可以使用 volatile 禁用指令重排序。更多细节可以参考java单例模式一文。

(5) 惰性加载方式

由于加锁会带来性能损耗,最好的办法还是期望实现一种无锁的设计,且又能实现延迟加载。对Java语言来说,静态内部类会延迟加载(对C#语言来说,内部类会延迟加载)。可以利用这一特性,实现单例。

// 1. 定义单例类,提供全局唯一访问点,保证实例唯一
public class LazyLoadingSingleton {// (2) 私有构造方法private LazyLoadingSingleton() {}// (3) 定义静态方法,提供全局唯一访问点public static LazyLoadingSingleton getInstance() {// 第一调用静态类成员或方法时,才加载静态内部类,实现了延迟加载return Holder.instance;}public void foo() {System.out.println("---------do some thing in a LazyLoadingSingleton instance---------");}// (1) 声明私有静态内部类,并提供私有成员变量private static class Holder {private static LazyLoadingSingleton instance = new LazyLoadingSingleton();}
}
// 2. 客户端调用
public class LazyLoadingSingletonClient {public void test() {// (1) 获取实例LazyLoadingSingleton instance = LazyLoadingSingleton.getInstance();// (2) 调用实例方法instance.foo();}
}

很多框架代码都会引入静态内部类,实现延迟加载。更多静态内部类的使用细节可以参考笔者之前的文章。

适用场景

在以下情况下可以使用单例模式:
(1) 如果系统只需要一个实例对象,则可以考虑使用单例模式。
如提供一个唯一的序列号生成器,或者需要考虑资源消耗太大而只允许创建一个对象。
(2) 如果需要调用的实例只允许使用一个公共访问点,则可以考虑使用单例模式。
(3) 如果一个系统只需要指定数量的实例对象,则可以考虑扩展单例模式。
可以在单例模式中,通过限制实例数量实现多例模式。

优缺点

单例模式模式有以下优点:
(1) 提供了对唯一实例的受控访问。
因为单例类封装了它的唯一实例,所以它可以严格控制客户怎样以及何时访问它。
(2) 节约系统资源。由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象,单例模式无疑可以提高系统的性能。
(3) 允许可变数目的实例。可以基于单例模式进行扩展,使用与单例控制相似的方法来获得指定个数的对象实例。
但是单例模式模式也存在以下缺点:
(1) 违反了单一职责原则。单例类的职责过重,既充当工厂角色,提供了工厂方法,同时又充当产品角色,包含一些业务方法,将产品的创建产品本身的功能融合到一起,在一定程度上违背了单一职责原则。
(2) 单例类扩展困难。由于单例模式中没有抽象层,且继承困难,所以单例类的扩展有很大的困难。
(3) 滥用单例模式带来一些负面问题,如过多的创建单例,会导致这些单例类一直无法释放且占用内存空间,另外对于一些不频繁使用的但占用内存空间较大的对象,也不宜将其创建为单例。而且现在很多面向对象语言(如Java、C#)都提供了自动垃圾回收的技术。

参考

《设计模式:可复用面向对象软件的基础》 Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides 著 李英军, 马晓星 等译
https://design-patterns.readthedocs.io/zh_CN/latest/creational_patterns/singleton.html 单例模式
https://refactoringguru.cn/design-patterns/singleton 单例模式
https://www.runoob.com/design-pattern/singleton-pattern.html 单例模式
https://www.cnblogs.com/adamjwh/p/9033554.html 单例模式
https://blog.csdn.net/czqqqqq/article/details/80451880 单例模式

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/20782.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Tessent ScanATPG appendix3 about dft signals

add_dft_signals 用于请求附加的静态,动态的DFT信号;存在一些已经定义好的DFT信号可以用来控制电路; dft_signals 分三大类: global_dft_control 用于控制global resources 比如时钟,电源管理的电路;logic test control 用于控制电路中的部分点的值; 包括int_ltest_en ext_l…

71. 简化路径

题目链接:力扣 解题思路: 以 "/" 对路径字符串进行分割,得到分割后的数组split,那么数组中每个元素就是一级路径的名称对split进行遍历:使用一个队列deque保存最终的每一个目录 如果当前字符串是 "..&…

“从零开始学习Spring Boot:构建高效的Java应用程序“

标题:从零开始学习Spring Boot:构建高效的Java应用程序 摘要:本篇博客将带你从零开始学习如何使用Spring Boot构建高效的Java应用程序。我们将讨论Spring Boot的基本概念和特性,并提供一个简单的示例代码来帮助你入门。 正文&am…

1.两数相加

力扣题目链接:https://leetcode.cn/problems/two-sum/ 给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。 你可以假设每种输入只会对应一个答案。但是,数组中同一…

【c++】rand()随机函数的应用(二)——舒尔特方格数字的生成

目录 一、舒尔特方格简介 二、如何生成舒尔特方格 (一)线性同余法 1、利用线性同余法生成随机数序列的规律 (1) 当a和c选取合适的数时,可以生成周期为m的随机数序列 (2) 种子seed取值也是有周期的 2、利用线性同余法生成5阶舒尔特方格…

mysql的基础面经-索引、事务

1 聚簇索引 1 和主键索引的关系 2 和非聚簇索引的关系,其叶子节点存储的是聚簇索引中的主键 3 索引覆盖机制使得非聚簇索引不用回表二次查询 2 举一个使用索引覆盖的例子 我的项目中没有使用到覆盖索引,但是可以举一个例子,比如我直接为年…

app自动化测试

在实习过程中,我接触到了一些SDL安全提测的工作。原来我是学web端渗透比较多的,移动端这块基本没怎么试过手,结果刚开始一直踩坑,连抓包都抓不到(T▽T)。 下面记录下我遇到的部分问题和解决方法&#xff0c…

誉天程序员-瀑布模型-敏捷开发模型-DevOps模型比较

文章目录 2. 项目开发-开发方式2.1. 瀑布开发模型2.2. 敏捷开发模型2.3. DevOps开发模型2.4. 区别 自增主键策略1、数据库支持主键自增自增和uuid方案优缺点 2. 项目开发-开发方式 由传统的瀑布开发模型、敏捷开发模型,一跃升级到DevOps开发运维一体化开发模型。 …

本地部署 audiocraft

本地部署 audiocraft 1. 什么是 audiocraft2. Github 地址3. 安装 Miniconda34. 创建虚拟环境5. 部署 audiocraft6. 启动 MusicGen7. 访问 MusicGen 1. 什么是 audiocraft Audiocraft 是一个通过深度学习进行音频处理和生成的库。它具有最先进的 EnCodec 音频压缩器/分词器&am…

【MySQL】MVCC的实现原理

MVCC的实现原理 1.前期准备1.2.隐式字段1.3.undo log日志1.4.readView 2.MVCC的实现流程2.1.R C(读已提交---隔离级别)2.2.R R(可重复读---隔离级别) 3.面试题---->事务中的隔离性是如何保证的呢?(你解释一下MVCC) …

2023年第四届“华数杯”数学建模思路 - 案例_ ID3-决策树分类算法

文章目录 0 赛题思路1 算法介绍2 FP树表示法3 构建FP树4 实现代码 0 赛题思路 (赛题出来以后第一时间在CSDN分享) https://blog.csdn.net/dc_sinor?typeblog 1 算法介绍 FP-Tree算法全称是FrequentPattern Tree算法,就是频繁模式树算法&…

Kubernetes高可用集群二进制部署(二)ETCD集群部署

Kubernetes概述 使用kubeadm快速部署一个k8s集群 Kubernetes高可用集群二进制部署(一)主机准备和负载均衡器安装 Kubernetes高可用集群二进制部署(二)ETCD集群部署 Kubernetes高可用集群二进制部署(三)部署…

MySQL数据库安装(二)

夕阳留恋的不是黄昏,而是朝阳 上一章简单介绍了MySQL数据库概述(一), 如果没有看过, 请观看上一章 一. MySQL 卸载 一.一 停止MySQL服务 在卸载之前,先停止MySQL8.0的服务。按键盘上的“Ctrl Alt Delete”组合键,打开“任务管理器”对话…

【LeetCode-中等】剑指 Offer 35. 复杂链表的复制(详解)

目录 题目 方法1:错误的方法(初尝试) 方法2:复制、拆开 方法3:哈希表 总结 题目 请实现 copyRandomList 函数,复制一个复杂链表。在复杂链表中,每个节点除了有一个 next 指针指向下一个节…

数字滚动变化-指令形式

话不多说&#xff0c;直接上代码 <template><divv-data-scroll"{target: 100speed: 1000}">100</div> </template><script setup lang"ts"> import { DirectiveBinding } from vue;function dataScroll(el: HTMLElement, …

2023电赛E题视觉部分

该部分主要要完成正方形区域的识别&#xff0c;并返回对应的坐标&#xff0c;但是由于距离1m&#xff0c;过远。因此需要引入图像增强&#xff0c;下面代码完成基本流程测试&#xff0c;仅供参考&#xff1a; import sensor import image import time # 初始化摄像头 senso…

WEB 文件包含 /伪协议

首先谈谈什么是文件包含 WEB入门——文件包含漏洞与PHP伪协议_文件包含php伪协议_HasntStartIsOver的博客-CSDN博客 文件包含 程序员在编写的时候 可能写了自己的 函数 如果想多次调用 那么就需要 重新写在源代码中 太过于麻烦了只需要写入 funcation.php然后在需要引用的地…

Vue3使用Mitt中央事件总线实现组件之间通讯(发布订阅库)

前言 现在的项目慢慢从 Vue2 升级到 Vue3 了&#xff0c;之前 Vue2 自带的中央事件总线是 EventBus&#xff0c;在 Vue3 中已经被移除了&#xff0c;官方推荐使用 Mitt 发布订阅库。在此简单记录一下 Mitt 的使用方式。 一、导入依赖 npm i mitt -D 二、全局引入 &#xf…

【iOS】—— 循环引用问题总结

循环引用 文章目录 循环引用1.自循环引用2.相互循环引用3.多循环引用 常见的循环引用问题1.delegate解决方法&#xff1a; 2.block解决方法&#xff1a;1.强弱共舞2.把当前类作为block的参数3.用__block修饰变量&#xff0c;在block内部置nil 3.NSTimer解决方案&#xff1a;1.使…

linux ftp

使用ftp连接本机进行文件传输 1、下载vsftpd服务器程序 apt install vsftpd 2、使用tcp抓包 tcpdump -nt -i lo port 20 在FTP连接到本地主机&#xff08;127.0.0.1&#xff09;时&#xff0c;数据可能通过本地回环接口&#xff08;loopback interface&#xff09;传输&…