C++设计模式创建型模式———单例模式

文章目录

  • 一、引言
  • 二、懒汉模式
  • 三、饿汉模式
  • 四、C++11 的线程安全单例
  • 五、与其他模式的关系
  • 六、总结

一、引言

单例模式是一种创建型设计模式, 让你能够保证一个类只有一个实例, 并提供一个访问该实例的全局节点。

在一个项目中,全局范围内,某个类的实例有且仅有一个,通过这个唯一实例向其他模块提供数据的全局访问,这种模式就叫单例模式。单例模式的典型应用就是任务队列。

在使用单例模式时,核心是确保类的实例只能有一个,就像“独生子女”一样,不能有其他兄弟姐妹。为了实现这一点,需要采取以下措施:

  1. 构造函数私有化:将构造函数(拷贝构造和构造)设置为私有,使得外部无法直接创建实例。这样在类内部可以控制只创建一次实例。
  • 由于使用者在类外部不能使用构造函数,所以在类内部创建的这个唯一的对象必须是静态的,这样就可以通过类名来访问了,为了不破坏类的封装,我们都会把这个静态对象的访问权限设置为私有的。
  • 在类中只有它的静态成员函数才能访问其静态成员变量,所以可以给这个单例类提供一个静态函数用于得到这个静态的单例对象。
  1. 静态实例:由于构造函数是私有的,需要在类内部通过一个静态变量来创建唯一的实例,并且使其私有化,以保证外部无法访问或修改。
  2. 提供静态访问方法:通过一个静态成员函数,外部可以访问这个静态的单例对象。这种方式既能保证实例的唯一性,又保持了封装性。
  3. 禁止或私有化拷贝和赋值:通过将拷贝构造函数和拷贝赋值操作符删除(使用 = delete),防止通过拷贝或赋值创建新的实例。这样可以杜绝实例化多个对象的可能性。

通过这些措施,我们可以确保类只能有一个实例,从而实现了一个线程安全且高效的单例模式。

在这里插入图片描述

单例模式的主要目的是确保一个类在程序运行期间只有一个实例,并提供一个全局访问点来访问该实例。单例模式可以通过懒汉式(Lazy Initialization)和饿汉式(Eager Initialization)两种方式实现。下面分别介绍这两种实现方法。


二、懒汉模式

懒汉式单例是指在需要时才创建单例对象。它的优点是延迟加载,不会在程序启动时就占用内存。缺点是需要考虑多线程环境下的安全问题。

// 懒汉模式
class Singleton
{
public:
    // = delete 代表函数禁用, 也可以将其访问权限设置为私有
    Singleton(const Singleton& obj) = delete;
    Singleton& operator=(const Singleton& obj) = delete;
    // 获取单例实例的静态方法
    static Singleton* getInstance() {
        // 双重检查锁定,确保多线程下的线程安全性
        if (instance == nullptr) {
            std::lock_guard<std::mutex> lock(mutex_);
            if (instance == nullptr) {
                instance = new Singleton();
            }
        }
        return instance;
    }
private:
    Singleton() = default; // 私有化构造函数
    static Singleton* instance; // 单例实例指针
     static std::mutex mutex_; // 互斥锁,保证线程安全
};
// 初始化静态成员变量
Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mutex_;

在以上代码中,getInstance() 方法通过双重检查锁定(Double-Checked Locking)确保在多线程环境下的安全性,同时避免每次获取实例时都加锁。

双重检查锁定是一种用于懒汉式单例模式的优化技术,主要目的是在多线程环境下创建单例对象时,既保证线程安全性,又避免不必要的加锁操作,提高性能。

  1. 为什么需要双重检查锁定?

当我们在多线程环境中创建单例时,如果不进行适当的同步控制,可能会导致多个线程同时创建实例,从而产生多个对象,违背了单例模式的初衷。为了解决这个问题,我们通常会对获取实例的过程加锁,但是简单的加锁会带来性能问题。

假设我们没有使用双重检查锁定,而是简单地在 getInstance 方法中对整个创建过程加锁:

static Singleton* getInstance() {
    std::lock_guard<std::mutex> lock(mutex_);
    if (instance == nullptr) {
        instance = new Singleton();
    }
    return instance;
}

上面的代码可以保证线程安全性,但每次调用 getInstance 方法时都会加锁,即使实例已经被创建。加锁和解锁是一个耗时操作,会影响性能。

  1. 双重检查锁定是如何优化的?

双重检查锁定通过在加锁前后分别检查实例是否为空,减少了不必要的加锁操作。具体步骤如下:

static Singleton* getInstance() {
    // 第一次检查(不加锁)
    if (instance == nullptr) {
        // 加锁,防止多个线程同时创建实例
        std::lock_guard<std::mutex> lock(mutex_);
        
        // 第二次检查(加锁后)
        if (instance == nullptr) {
            instance = new Singleton();
        }
    }
    return instance;
}
  • 第一次检查:在加锁前检查 instance 是否为空。如果已经有实例了,就直接返回,无需加锁。
  • 加锁:如果 instance 为空,说明实例还没有被创建,进行加锁操作,确保只有一个线程能够进入创建实例的代码块。
  • 第二次检查:加锁后,再次检查 instance 是否为空。这样做是因为在第一个线程加锁前,其他线程可能已经创建了实例。只有在 instance 仍然为空时,才会创建新实例。
  1. 双重检查锁定的工作原理
  • 性能优化:大多数情况下,getInstance 方法在实例已经存在时,不会进行加锁操作。这显著提高了性能,尤其是在实例已经被创建后,其他线程只需要进行第一次检查即可返回实例。

  • 线程安全:在实例未创建时,通过加锁保证了只有一个线程可以创建实例,其他线程会被阻塞在锁外,确保了单例对象只被创建一次。


三、饿汉模式

饿汉模式就是在类加载的时候立刻进行实例化,这样就得到了一个唯一的可用对象。关于这个饿汉模式的类的定义如下:

饿汉式单例是在类加载时就创建实例。它的优点是不需要考虑多线程同步问题,因为在类加载时就完成了初始化;缺点是无论是否使用都会占用内存。也就是没有线程安全问题。

// 饿汉模式
class Singleton
{
public:
    // = delete 代表函数禁用, 也可以将其访问权限设置为私有
    Singleton(const Singleton& obj) = delete;
    Singleton& operator=(const Singleton& obj) = delete;
    static Singleton* getInstance()
    {
        return instance;
    }
private:
    Singleton() = default;
    static Singleton* instance;
};
// 静态成员初始化放到类外部处理
Singleton* Singleton::instance = new Singleton;

int main()
{
    Singleton* obj = Singleton::getInstance();
}

在这个例子中,Singleton 类的实例在程序启动时就被创建好了,不需要加锁。每次调用 getInstance() 时,都会返回相同的实例。

注:类的静态成员变量在使用之前必须在类的外部进行初始化才能使用。

在调用getInstance()函数获取单例对象的时候,如果在单线程情况下是没有什么问题的,如果是多个线程,调用这个函数去访问单例对象就有问题了。假设有三个线程同时执行了getInstance()函数,在这个函数内部每个线程都会new出一个实例对象。此时,这个任务队列类的实例对象不是一个而是3个,很显然这与单例模式的定义是相悖的。


四、C++11 的线程安全单例

在实现懒汉模式的单例的时候,相较于双重检查锁定模式有一种更简单的实现方法并且不会出现线程安全问题,那就是使用静态局部局部对象。

自C++11起,可以利用静态局部变量的特性实现线程安全的单例模式。这种方式既简单又安全:

#include <iostream>

class Singleton {
public:
    static Singleton& getInstance() {
        static Singleton instance; // C++11保证局部静态变量的线程安全
        return instance;
    }

    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

    void show() {
        std::cout << "Singleton instance: " << this << std::endl;
    }

private:
    Singleton() = default;
};

int main() {
    Singleton& s1 = Singleton::getInstance();
    Singleton& s2 = Singleton::getInstance();
    s1.show();
    s2.show();
    return 0;
}

在这个实现中,getInstance() 方法中的静态局部变量 instance 只会在第一次调用时初始化,并且C++11保证其线程安全。因此,这种方式在多线程环境中也是安全的,而且代码简洁易读。

这种方式不仅简化了代码,还保证了线程安全性,是在现代C++中推荐使用的单例模式实现方式。

  • 双重检查锁定主要用于在多线程环境下的懒汉式单例,避免重复创建实例,同时减少不必要的加锁操作。
  • 它通过两次检查(一次在加锁前,一次在加锁后),确保只有在实例未创建时才进行加锁和实例化,从而提高性能。
  • 在C++11及以后,可以直接使用静态局部变量的特性来实现线程安全的单例,而不需要使用双重检查锁定。

五、与其他模式的关系

  • 外观模式类通常可以转换为单例模式类, 因为在大部分情况下一个外观对象就足够了。
  • 如果你能将对象的所有共享状态简化为一个享元对象, 那么享元模式就和单例类似了。 但这两个模式有两个根本性的不同。
    1. 只会有一个单例实体, 但是享元类可以有多个实体, 各实体的内在状态也可以不同。
    2. 单例对象可以是可变的。 享元对象是不可变的。
  • 抽象工厂模式、 生成器模式和原型模式都可以用单例来实现。

六、总结

  • 懒汉式单例适用于需要延迟加载实例的场景,但在多线程环境下需要加锁。
  • 饿汉式单例简单且不需要考虑线程安全,但在不需要时也会占用内存。
  • C++11静态局部变量单例实现方式最为简洁,并且在多线程环境下是安全的,是推荐使用的方法。

懒汉模式的缺点是在创建实例对象的时候有安全问题,但这样可以减少内存的浪费(如果用不到就不去申请内存了)。饿汉模式则相反,在我们不需要这个实例对象的时候,它已经被创建出来,占用了一块内存。对于现在的计算机而言,内存容量都是足够大的,这个缺陷可以被无视。

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

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

相关文章

HTML+CSS实现超酷超炫的3D立方体相册

效果演示 HTML和CSS实现一个简单的3D立方体加载动画的相册。它使用了HTML来构建立方体的结构&#xff0c;并通过CSS来添加样式和动画效果。 HTML <div class"loader3d"><div class"cube"><div class"face"><img src&qu…

如何对pdf文件进行加密?pdf文件加密全攻略与深度解析(5个方法)

如何对pdf文件进行加密&#xff1f; 只见&#xff0c;在深夜的情报局里&#xff0c;特工小李将一份绝密PDF文件放在保险箱内&#xff0c;以为这样就天衣无缝了。 细细推敲&#xff0c;漏洞百出&#xff1a; 如果钥匙被盗呢&#xff1f;如果被神匠破解出密码呢&#xff1f;如果…

java 提示 避免用Apache Beanutils进行属性的copy。

避免用Apache Beanutils进行属性的copy。 Inspection info: 避免用Apache Beanutils进行属性的copy。 说明&#xff1a;Apache BeanUtils性能较差&#xff0c;可以使用其他方案比如Spring BeanUtils, Cglib BeanCopier。 TestObject a new TestObject(); TestObject b new Te…

linux指令笔记

bash命令行讲解 lyt &#xff1a;是用户名 iZbp1i65rwtrfbmjetete2b2Z :这个是主机名 ~ &#xff1a;这个是当前目录 $ &#xff1a;这个是命令行提示符 每个指令都有不同的功能&#xff0c;大部分指令都可以带上选项来实现不同的效果。 一般指令和选项的格式&#xff1a;…

【openEuler/Centos】yum安装软件报Error: GPG check FAILED【分析根因弄明白,亲测有效不浪费时间】

yum安装软件报Error: GPG check FAILED 环境信息&#xff1a;cat /etc/openEuler-release openEuler release 22.03 (LTS-SP1) 报错信息 The downloaded packages were saved in cache until the next successful transaction. You can remove cached packages by executin…

当我们在微服务中使用API网关时,它是否会成为系统的瓶颈?这种潜在的瓶颈如何评估和解决?如何在微服务架构中保证高效请求流量?|API网关|微服务|异步处理

目录 1. API网关在微服务中的角色与重要性 2. API网关瓶颈的评估 2.1 请求延迟分析 2.2 并发请求量监控 2.3 内存和CPU使用情况 2.4 限流和熔断机制评估 2.5 日志分析 3. API网关瓶颈的解决方案 3.1 缓存机制优化 3.2 负载均衡优化 3.3 异步处理与消息队列 3.4 限流…

记录如何在RK3588板子上跑通paddle的OCR模型

官网文档地址 rknn_zoo RKNPU2_SDK RKNN Model Zoo 一、PC电脑是Ubuntu22.04系统中完成环境搭建(板子是20.04&#xff09; 安装模型转换环境 ​conda create -n rknn2 python3.10 conda activate rknn2 安装Ubuntu依赖包 su…

STM32 第18章 SysTick--系统定时器

时间:2024.10.26-10.27 参考资料: 《零死角玩转STM32》“SysTick--系统定时器”章节 一、学习内容 1.SysTick简介 1.1 SysTick: 系统定时器,24位,只能递减,存在于内核,嵌套在NVIC中,所有的Cortex-M内核的单片机都具有这个定时器。 官方参考手册里的介绍: 系统嘀…

riscv uboot 启动流程分析 - SPL启动流程

分析uboot 启动流程硬件&#xff1a;启明智显推出M4核心板 &#xff08;https://gitee.com/qiming-zhixian/m4-openwrt&#xff09; 1.U-boot和SPL概述 U-Boot 分为 uboot-spl 和 uboot 两个组成部分。SPL 是 Secondary Program Loader 的简称&#xff0c;第二阶段程序加载器。…

重塑在线软件开发新纪元:集成高效安全特性,深度解析与评估会员与促销管理系统的系统架构设计

案例 阅读以下关于软件架构设计与评估的叙述&#xff0c;回答问题1和问题2。 【题目】 某电子商务公司拟升级其会员与促销管理系统&#xff0c;向用户提供个性化服务&#xff0c;提高用户的粘性。在项目立项之初&#xff0c;公司领导层一致认为本次升级的主要目标是提升会员管…

简单的udp程序

文章目录 1. 预备知识1.1 源IP地址和目的IP地址1.2 端口号1.3 套接字初识1.4 tcp协议和udp协议简单认识1.5 网络字节序 2. udp程序2.1 创建套接字&#xff08;socket&#xff09;的系统调用2.2 bind()2.2.1 初始化一个sockaddr_in结构体2.2.2 inet_addr函数2.2.3 0.0.0.02.2.4 …

深入解析东芝TB62261FTG,步进电机驱动方案

TB62261FTG是一款由东芝推出的两相双极步进电机驱动器&#xff0c;采用了BiCD工艺&#xff0c;能够提供高效的电机控制。这款芯片具有多种优秀的功能&#xff0c;包括PWM斩波、内置电流调节、低导通电阻的MOSFET以及多种步进操作模式&#xff0c;使其非常适合用于需要精确运动控…

一步一步从微信小程序获取asp.net Core API的数据

前面我们说过&#xff0c;如何使用微信小程序获取asp.net的数据&#xff0c;这里我们继续介绍如何获取asp.net core api的数据。两者之间还是有一些差别的。本篇博文旨在详细介绍如何一步一步从微信小程序获取asp.net Core API的数据。 文章目录 一、建立并了解asp.net core we…

RabbitMQ集群搭建及使用

1. 概述 前提条件&#xff1a;linux服务器下已经安装好了docker服务。 本文档将搭建一个三台RabbitMQ的集群&#xff0c;包括三个RabbitMQ容器安装在同一服务器和三台不同的服务器。 2. 集群搭建 在一台服务器上创建三个RabbitMQ容器。 2.1.1. 创建容器 执行以下命令创建三…

合理利用IPIDEA代理IP,优化数据采集效率!

一、前言 在全球化与信息化交织的当代社会&#xff0c;数据已成为驱动商业智慧与技术革新的核心引擎。网络&#xff0c;作为信息汇聚与交流的枢纽&#xff0c;不仅是人们获取知识的窗口&#xff0c;更是商业活动与技术创新的广阔舞台。在这个信息繁荣的时代&#xff0c;Python…

Docker 实践与应用举例教程:从入门到精通

Docker 实践与应用举例教程&#xff1a;从入门到精通 引言 在现代软件开发中&#xff0c;Docker 已成为一种不可或缺的工具。它通过容器化技术简化了应用的部署、管理和扩展&#xff0c;极大地提高了开发和运维的效率。本文将详细介绍 Docker 的基本概念、安装步骤、常用命令…

开放式耳机哪个品牌音质好?音质最好的开放式耳机推荐!

如今&#xff0c;开放式耳机市场日益繁荣&#xff0c;成为了众多音乐爱好者和追求舒适佩戴体验者的新宠。然而&#xff0c;面对琳琅满目的品牌和产品&#xff0c;消费者往往陷入选择的困境。音质&#xff0c;作为衡量一款耳机优劣的关键因素&#xff0c;更是备受关注。究竟哪个…

反编译华为-研究功耗联网监控日志

摘要 待机功耗中联网目前已知的盲点&#xff1a;App自己都不知道的push类型的被动联网、app下载场景所需时长、组播联网、路由器打醒AP。 竞品 策略 华为 灭屏使用handler定时检测&#xff08;若灭屏30分钟内则周期1分钟&#xff0c;否则为2分钟&#xff09;&#xff0c;检…

【Unity踩坑】UWP应用未通过Windows应用认证:API不支持

在将Unity项目导出为XAML类型的UWP项目后&#xff0c;通过Visual Studio打包成功&#xff0c;但在进行Windows应用认证时结果是Failed。 其中的错误是某些dll里用到了Windows SDK不支持的API。 本次问题中涉及到的具体dll有两个&#xff1a;gilzoide-sqlite-net.dll和D3D12Cor…

【Linux网络】传输层协议UDP与TCP

W...Y的主页 &#x1f60a; 代码仓库分享 &#x1f495; 目录 传输层 再谈端口号 ​编辑 端口号范围划分 认识知名端口号(Well-Know Port Number) netstat pidof UDP协议 UDP协议端格式 UDP的特点 面向数据报 UDP的缓冲区 UDP使用注意事项 基于UDP的应用层协议…