单例模式(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.mfbz.cn/a/54367.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

小区智能电动汽车充电桩如何收费盈利?

摘要:智能用电小区是国家电网为了研究智能电网智能用电的先进技术如何运用于居民区,提高人民的生活水平,提高电网智能化水平以及提升用电服务质量而进行的一项尝试。电动汽车作为智能用电小区建设的一个组成部分同样也逐渐被纳入发展规划&…

wireshark导出H264裸流

导出H264裸流 安装wireshark下载rtp_h264_extractor.lua脚本配置lua脚本重启wireshark筛选 安装wireshark 下载抓包工具:首先,您需要下载并安装一个网络抓包工具,例如Wireshark(https://www.wireshark.org)或tcpdump&…

26 用lsqnonlin求解最小二乘问题(matlab程序)

1.简述 函数语法 x lsqnonlin(fun,x0) 函数用于: 解决非线性最小二乘(非线性数据拟合)问题 解决非线性最小二乘曲线拟合问题的形式 变量x的约束上下限为ub和lb, x lsqnonlin(fun,x0)从x0点开始,找到fun中描述的函数的最小平方和。函数fu…

Linux操作系统(一):详解CPU

学习操作系统往往需要先学习CPU相关知识,然后再学习操作系统的结构,主要是因为操作系统是运行在 CPU 上的核心软件,它通过与 CPU 的交互来管理计算机的硬件资源,执行各种系统服务,并为用户和应用程序提供接口和功能。 …

心理测量平台目录遍历

你知道,幸福不仅仅是吃饱穿暖,而是勇敢的战胜困难。 漏洞描述 心理测量平台存在目录遍历漏洞,攻击者可利用该漏洞获取敏感信息。 漏洞复现 访问目录遍历漏洞路径: /admin/漏洞证明: 文笔生疏,措辞浅薄…

Prometheus中的关键设计

1、标准先行,注重生态 Prometheus 最重要的规范就是指标命名方式,数据格式简单易读。比如,对于应用层面的监控,可以要求必须具备这几个信息。 指标名称 metric Prometheus 内置建立的规范就是叫 metric(即 __name__…

clickhouse查询缓存

为了实现最佳性能,数据库需要优化其内部数据存储和处理管道的每一步。但是数据库执行的最好的工作是根本没有完成的工作!缓存是一种特别流行的技术,它通过存储早期计算的结果或远程数据来避免不必要的工作,而访问这些数据的成本往…

Redis以及Java使用Redis

一、Redis的安装 Redis是一个基于内存的 key-value 结构数据库。 基于内存存储,读写性能高 适合存储热点数据(热点商品、资讯、新闻) 企业应用广泛 官网:https://redis.io 中文网:https://www.redis.net.cn/ Redis…

微信小程序监测版本更新

在index.js里面 不放到app.js里面是因为有登录页面,在登录页面显示更新不太友好 onShow() {const updateManager wx.getUpdateManager()// 请求完新版本信息的回调updateManager.onCheckForUpdate(res > {if (res.hasUpdate) {// 新版本下载成功updateManage…

B076-项目实战--宠物上下架 展示 领养 收购订单

目录 上下架功能提供后台宠物列表实现 前台展示前台宠物列表和详情展示店铺展示 领养分析前台后端PetControllerPetServiceImpl 订单需求分析可能产生订单的模块订单模块额外功能 订单设计表设计流程设计 集成基础代码收购订单创建订单前端后端 上下架功能提供 后台宠物列表实…

解决AttributeError: ‘DataParallel‘ object has no attribute ‘xxxx‘

问题描述 训练模型时,分阶段训练,第二阶段加载第一阶段训练好的模型的参数,接着训练 第一阶段训练,含有代码 if (train_on_gpu):if torch.cuda.device_count() > 1:net nn.DataParallel(net)net net.to(device)第二阶段训练…

数据结构-链表结构-单向链表

链表结构 说到链表结构就不得不提起数据结构,什么是数据结构?就是用来组织和存储数据的某种结构。那么到底是某种结构呢? 数据结构分为: 线性结构 数组,链表,栈,队列 树形结构 二叉树&#x…

P1219 [USACO1.5] 八皇后 Checker Challenge

题目 思路 非常经典的dfs题&#xff0c;需要一点点的剪枝 剪枝①&#xff1a;行、列&#xff0c;对角线的标记 剪枝②&#xff1a;记录每个皇后位置 代码 #include<bits/stdc.h> using namespace std; const int maxn105; int a[maxn];int n,ans; bool vis1[maxn],vis…

解决:请求的资源[/xxx/]不可用 描述 源服务器未能找到目标资源的表示或者是不愿公开一个已经存在的资源表示。

1. 复现错误 今天启动jsp servlet项目&#xff0c;却报出如下错误&#xff1a; 2. 分析问题 报出该错误&#xff0c;一般是tomcat无法访问webapp下的文件&#xff0c;特采用如下方法解决问题。 检查涉及到jdk的版本号是否一致&#xff0c;我的是1.8的版本&#xff0c;所以&am…

AI革命:揭开微软无与伦比的AI技术面纱

来源&#xff1a;猛兽财经 作者&#xff1a;猛兽财经 2023年7月25日&#xff0c;全球科技行业的领导者之一微软(MSFT)公布了其2023财年第四季度的财报。 除了举世闻名的Windows操作系统&#xff0c;微软还通过笔记本电脑、个人电脑和服务器等产品改变了世界&#xff0c;该公司…

QMessageBox类

QMessageBox类 静态方法例子 静态方法 调用这一些静态成员函数&#xff0c;就可以得到模态提示框 枚举值为&#xff1a; 例子 头文件&#xff1a; #ifndef MAINWINDOW_H #define MAINWINDOW_H#include <QMainWindow> #include <QMessageBox>QT_BEGIN_NAMESPACE…

gorm基础

Gorm 官方文档&#xff1a;https://gorm.io/zh_CN/ 安装 go get -u github.com/jinzhu/gorm连接数据库 连接不同的数据库都需要导入对应数据的驱动程序&#xff0c;GORM已经贴心的为我们包装了一些驱动程序&#xff0c;只需要按如下方式导入需要的数据库驱动即可&#xff1…

Android Studio 关于BottomNavigationView 无法预览视图我的解决办法

一、前言&#xff1a;最近在尝试一步一步开发一个自己的软件&#xff0c;刚开始遇到的问题就是当我们引用 com.google.android.material.bottomnavigation.BottomNavigationView出现了无法预览视图的现象&#xff0c;我也在网上查了很多中解决方法&#xff0c;最后在执行了如下…

会议oa系统项目部署流程

目录 1.项目部署环境 2.初始化数据库 2.1获取数据库脚本 2.2创建数据库 1.创立数据库连接 2.创建数据库&#xff0c;命名 3.运行sql文件 4.查看导入数据 ​编辑 ​编辑 3项目环境部署 3.1导入项目资源 3.2加载框架 加载成功标志 服务器配置&#xff08;用来保存排…

glide加载content://com.android.contacts图片源码粗略梳理

获取链路是这样的&#xff1b; UriLoader类里定义了协议头&#xff1a; 里面有个内部类StreamFactory&#xff1a; 通过StreamLocalUriFetcher类的loadResource方法获取InputStream然后把流转换成为图片&#xff1b; 在这里作个草稿笔记给自己看