设计模式13-单件模式

设计模式13-单件模式

  • 写在前面
    • 对象性能模式
    • 典型模式
      • 1. 单例模式(Singleton Pattern)
      • 2. 享元模式(Flyweight Pattern)
      • 3. 原型模式(Prototype Pattern)
      • 4. 对象池模式(Object Pool Pattern)
      • 5. 延迟初始化(Lazy Initialization)
      • 6. 虚拟代理(Virtual Proxy)
      • 7. 缓存(Caching)
      • 8. 数据传输对象(DTO,Data Transfer Object)
  • 动机
  • 单例模式的定义与结构
  • 单例模式的代码推导
      • 2. 线程安全版本,但锁的代价过高
      • 3. 双检查锁,但由于内存读写reorder不安全
    • 4. C++11版本之后的跨平台实现(volatile)
    • volatile关键字
      • 平台限制和使用场景
      • 多线程编程中的问题
      • 推荐的跨平台多线程同步机制
      • 总结
  • 单例模式的应用
  • 单例模式的特点总结
  • 补充一个单例模式的实现
      • 实现方式
        • 1. 第一种单例模式(使用 `std::atomic` 和 `std::mutex`)
        • 2. 第二种单例模式(使用 `std::call_once`)
      • 比较
      • 总结

写在前面

对象性能模式

  • 面向对象很好的解决了抽象的问题,但是必不可免的要付出一定的代价(类空间的重复分配等)。对于通常情况来讲,面向对象的成本大多都可以忽略不计。但某些情况面向对象所在的成本必须谨慎处理。
  • 设计模式中的对象性能模式是指那些专注于提高系统性能、优化资源使用和管理的设计模式。这些模式通过有效的对象创建、共享、复用和管理策略,减少内存占用、提高运行效率,从而提升整体系统性能。

典型模式

  • 单例模式(Singleton Pattern)
  • 享元模式(Flyweight Pattern)
    当然也有一些其他的模式。

设计模式中涉及对象性能优化的模式主要包括以下几种

1. 单例模式(Singleton Pattern)

单例模式确保一个类只有一个实例,并提供全局访问点。这种模式避免了重复创建对象的开销,特别是在需要频繁访问的场景中,如配置类、日志类等。

2. 享元模式(Flyweight Pattern)

享元模式通过共享对象来减少内存使用。它特别适用于有大量相似对象的场景,例如字符处理、图形绘制等。

3. 原型模式(Prototype Pattern)

原型模式通过复制现有对象来创建新对象,从而减少了创建新对象的开销。这种模式适用于创建代价高昂的对象,如大型数据结构、复杂对象等。

4. 对象池模式(Object Pool Pattern)

对象池模式维护一个对象池,复用池中的对象而不是每次都创建和销毁对象。它适用于对象创建和销毁成本较高的情况,如数据库连接、线程等。

5. 延迟初始化(Lazy Initialization)

延迟初始化是指对象的创建或初始化在其实际被使用时才进行。这可以减少程序启动时的资源占用,并将资源分配推迟到实际需要时。

6. 虚拟代理(Virtual Proxy)

虚拟代理是一种代理模式,通过代理对象来控制对实际对象的访问。虚拟代理可以延迟实际对象的创建或初始化,从而优化性能。例如,大型图片的加载可以通过虚拟代理延迟到实际需要时再进行。

7. 缓存(Caching)

缓存模式通过存储之前计算或创建的结果来减少重复计算或创建的开销。这种模式广泛应用于各种性能优化场景,如数据库查询结果缓存、计算结果缓存等。

8. 数据传输对象(DTO,Data Transfer Object)

DTO模式通过一次性传输批量数据来减少多次远程调用的开销。它适用于分布式系统中,通过减少网络通信次数来提高性能。

动机

在软件系统中经常有这样一些特殊的类必须保证他们在系统中只存在一个实例。才能保证他们的逻辑的正确性以及良好的效率。如何绕过常规的构造器提供一种机制来保证一个类只有一个实例呢?让使用者只需使用一个类?这显然不合理。因为这应该是类设计者的责任,而不是使用者的责任。模式就是为了解决这一类问题的模式。

more动机:

  • 在许多应用中,有一些对象需要全局唯一,比如配置文件管理器、日志管理器等。为了确保这些对象在整个应用中只存在一个实例,并且能被全局访问,我们需要一种机制来限制实例化次数并提供统一的访问方式。
  • 单例模式的主要动机是控制对象的实例化过程,确保一个类只有一个实例,并提供一个访问它的全局访问点

单例模式的定义与结构

定义
保证一个类仅有一个实力,并提供一个该实例的全局访问点。

结构:

  1. Singleton类:
    • 定义一个静态变量来保存类的唯一实例。
    • 提供一个静态方法用于创建或获取该实例。
    • 将构造函数设为私有,防止外部通过new操作符实例化该类。

以下是单例模式的UML图:

在这里插入图片描述

单例模式的代码推导

class Singleton{
private:Singleton();Singleton(const Singleton& other);
public:static Singleton* getInstance();static Singleton* m_instance;
};Singleton* Singleton::m_instance=nullptr;//线程非安全版本
Singleton* Singleton::getInstance() {if (m_instance == nullptr) {m_instance = new Singleton();}return m_instance;
}//线程安全版本,但锁的代价过高
Singleton* Singleton::getInstance() {Lock lock;if (m_instance == nullptr) {m_instance = new Singleton();}return m_instance;
}//双检查锁,但由于内存读写reorder不安全
Singleton* Singleton::getInstance() {if(m_instance==nullptr){Lock lock;if (m_instance == nullptr) {m_instance = new Singleton();}}return m_instance;
}//C++ 11版本之后的跨平台实现 (volatile)
std::atomic<Singleton*> Singleton::m_instance;
std::mutex Singleton::m_mutex;Singleton* Singleton::getInstance() {Singleton* tmp = m_instance.load(std::memory_order_relaxed);std::atomic_thread_fence(std::memory_order_acquire);//获取内存fenceif (tmp == nullptr) {std::lock_guard<std::mutex> lock(m_mutex);tmp = m_instance.load(std::memory_order_relaxed);if (tmp == nullptr) {tmp = new Singleton;std::atomic_thread_fence(std::memory_order_release);//释放内存fencem_instance.store(tmp, std::memory_order_relaxed);}}return tmp;
}

这段代码展示了不同版本的单例模式的实现,包括线程不安全版本、线程安全但性能不佳的版本、双检查锁版本以及C++11之后的跨平台实现。以下是对每个版本的详细说明:

##1. 线程不安全版本

class Singleton{
private:Singleton();Singleton(const Singleton& other);
public:static Singleton* getInstance();static Singleton* m_instance;
};Singleton* Singleton::m_instance = nullptr;Singleton* Singleton::getInstance() {if (m_instance == nullptr) {m_instance = new Singleton();}return m_instance;
}

说明:

  • 这个版本中,getInstance方法检查m_instance是否为空,如果为空则创建一个新的Singleton实例。
  • 这种实现方式在单线程环境中是安全的,但在多线程环境中可能导致多个线程同时创建多个实例,违背单例模式的初衷。

2. 线程安全版本,但锁的代价过高

Singleton* Singleton::getInstance() {Lock lock;if (m_instance == nullptr) {m_instance = new Singleton();}return m_instance;
}

说明:

  • 这个版本使用了锁(例如std::mutex)来确保线程安全。
  • 虽然保证了线程安全,但每次调用getInstance方法都需要获取锁,代价较高,可能影响性能。

3. 双检查锁,但由于内存读写reorder不安全

Singleton* Singleton::getInstance() {if (m_instance == nullptr) {Lock lock;if (m_instance == nullptr) {m_instance = new Singleton();}}return m_instance;
}

说明:

  • 双检查锁的目的是在进入锁之前先检查m_instance是否为空,以减少获取锁的次数,提高性能。
  • 但是,由于编译器和CPU可能对内存读写操作进行重新排序,这种实现方式可能不安全。因为在所有编译器中会对代码进行优化处理,对于new Singleton操作在某些情况下可能会先返回分配空间的指针,后调用构造函数,那么此时第二个线程可能在进行第一个实例判空判断时直接返回指针,但此时这个指针是不可用的,也就是说线程可能看到未完全构造的对象。

4. C++11版本之后的跨平台实现(volatile)

#include <atomic>
#include <mutex>class Singleton{
private:Singleton() {}Singleton(const Singleton& other) = delete;Singleton& operator=(const Singleton& other) = delete;
public:static Singleton* getInstance();static std::atomic<Singleton*> m_instance;static std::mutex m_mutex;
};std::atomic<Singleton*> Singleton::m_instance{nullptr};
std::mutex Singleton::m_mutex;Singleton* Singleton::getInstance() {Singleton* tmp = m_instance.load(std::memory_order_relaxed);std::atomic_thread_fence(std::memory_order_acquire); // 获取内存fenceif (tmp == nullptr) {std::lock_guard<std::mutex> lock(m_mutex);tmp = m_instance.load(std::memory_order_relaxed);if (tmp == nullptr) {tmp = new Singleton;std::atomic_thread_fence(std::memory_order_release); // 释放内存fencem_instance.store(tmp, std::memory_order_relaxed);}}return tmp;
}

说明:

  • 使用了std::atomicstd::mutex来实现线程安全的单例模式。
  • 通过std::atomic_thread_fence确保内存读写操作的顺序性,防止重排序问题。
  • 在获取锁之前,先尝试加载m_instance,并使用内存屏障(fence)确保正确的内存顺序。
  • 获取锁后再次检查m_instance,如果仍为空则创建实例并存储到m_instance中。

volatile关键字

volatile 关键字在 C++ 中的使用是为了告知编译器该变量可能会被异步地修改(例如,由硬件或其他线程),因此每次访问该变量时都需要重新读取,而不是使用缓存值。

然而,volatile 并不是用于实现线程安全的同步机制。在多线程编程中,volatile 不能保证变量访问的原子性或防止数据竞争。因此,对于跨平台的多线程编程,volatile 并不是一个可靠的选择。

平台限制和使用场景

  1. 硬件访问volatile 常用于访问硬件寄存器或与中断服务例程(ISR)共享的数据。在这些场景中,使用volatile是合适的,因为它能够防止编译器优化掉对这些变量的访问。

  2. 信号处理:在处理异步信号时,可以使用volatile变量标记信号状态。

多线程编程中的问题

在多线程编程中,volatile 并不能保证以下内容:

  1. 原子性:访问volatile变量的操作可能不是原子的。多个线程同时读取和写入volatile变量时,可能会出现竞态条件。
  2. 顺序性volatile 不能防止编译器、CPU 或内存系统对读写操作进行重新排序。因此,多个线程之间的操作顺序可能会混乱。
  3. 可见性:虽然volatile保证变量的最新值总是被读取,但它不能保证一个线程对变量的修改立即对其他线程可见。

推荐的跨平台多线程同步机制

为了实现跨平台的线程安全,应使用 C++11 及以后的标准库提供的原子操作和同步机制:

  1. std::atomic:提供原子操作,确保变量访问的原子性和线程之间的可见性。
  2. 内存顺序(Memory Order):C++11 标准引入了内存模型,通过std::memory_order枚举类型定义内存顺序,以控制内存操作的排序。
  3. 锁(Locks):使用std::mutexstd::lock_guard实现互斥锁,以保护共享数据。
  4. 条件变量(Condition Variables):使用std::condition_variable实现线程间的同步和通信。
    代码同上

总结

  • 线程不安全版本:简单但在多线程环境下不安全。
  • 线程安全版本(锁代价高):线程安全但性能较差,因为每次获取实例都需要获取锁。
  • 双检查锁版本:优化了性能,但存在内存重排序的问题。
  • C++11版本:使用std::atomic和内存屏障,解决了内存重排序的问题,是跨平台的线程安全实现。

通过这些不同版本的实现,我们可以看到单例模式在实际应用中的演变和优化,特别是针对线程安全和性能的考虑。

单例模式的应用

应用:

  • 配置管理: 需要在应用程序中全局共享配置实例。
  • 日志记录: 保证日志记录器实例在整个应用程序中唯一,统一管理日志记录。
  • 数据库连接池: 控制数据库连接池实例的唯一性,方便管理连接。
  • 线程池: 确保线程池实例的唯一性和复用。

单例模式的特点总结

特点总结:

  1. 唯一实例: 确保一个类只有一个实例,并提供全局访问点。
  2. 控制实例化: 通过私有构造函数和静态方法,控制对象的创建和获取。
  3. 延迟实例化: 在需要时才创建实例(懒加载),避免不必要的资源浪费。
  4. 线程安全: 需要特别注意多线程环境下的安全问题,可以通过双重检查锁(Double-Checked Locking)或静态内部类等方式实现线程安全。
  5. 全局访问: 提供一种全局访问点,使得可以方便地访问唯一实例。

单例模式是一种非常常用的设计模式,它在需要全局唯一实例的场景下非常有用。然而,使用单例模式时要注意可能带来的全局状态和并发问题,合理设计和实现才能确保系统的高效和稳定。

补充一个单例模式的实现

有两种单例模式实现的主要区别在于线程安全机制的实现方式和具体细节。以下是对这两种实现的详细比较:

实现方式

1. 第一种单例模式(使用 std::atomicstd::mutex
class Singleton {
private:Singleton() {}Singleton(const Singleton& other) = delete;Singleton& operator=(const Singleton& other) = delete;public:static Singleton* getInstance();static std::atomic<Singleton*> m_instance;static std::mutex m_mutex;
};std::atomic<Singleton*> Singleton::m_instance{nullptr};
std::mutex Singleton::m_mutex;Singleton* Singleton::getInstance() {Singleton* tmp = m_instance.load(std::memory_order_relaxed);std::atomic_thread_fence(std::memory_order_acquire);if (tmp == nullptr) {std::lock_guard<std::mutex> lock(m_mutex);tmp = m_instance.load(std::memory_order_relaxed);if (tmp == nullptr) {tmp = new Singleton;std::atomic_thread_fence(std::memory_order_release);m_instance.store(tmp, std::memory_order_relaxed);}}return tmp;
}

特点

  1. 使用 std::atomic 来存储单例实例,确保对实例的访问是原子操作。
  2. 使用内存屏障(memory fences)来确保内存操作的顺序。
  3. 双重检查锁定(Double-checked locking)机制,通过 std::mutex 确保在实例创建过程中只有一个线程进入临界区。
2. 第二种单例模式(使用 std::call_once
class CCalibrationJsonCoinfig {
private:static CCalibrationJsonCoinfig* instance;CCalibrationJsonCoinfig() {}~CCalibrationJsonCoinfig() {}public:static CCalibrationJsonCoinfig* getInstance() {std::call_once(flag, &CCalibrationJsonCoinfig::initInstance);return instance;}private:static void initInstance() {instance = new CCalibrationJsonCoinfig();}static std::once_flag flag;
};CCalibrationJsonCoinfig* CCalibrationJsonCoinfig::instance = nullptr;
std::once_flag CCalibrationJsonCoinfig::flag;

特点

  1. 使用 std::call_oncestd::once_flag 确保实例初始化函数只被调用一次,避免多线程环境下的重复初始化。
  2. 初始化逻辑更加简洁,利用 std::call_once 实现线程安全的单例模式。

比较

  1. 线程安全机制

    • 第一种方法:使用 std::atomicstd::mutex 实现双重检查锁定(Double-checked locking),需要手动管理锁和内存屏障,代码复杂度较高。
    • 第二种方法:使用 std::call_oncestd::once_flag 实现,标准库提供的简洁接口,避免了手动管理锁和内存屏障,代码简洁。
  2. 性能

    • 第一种方法:在高并发环境下,由于使用了双重检查锁定,只有在实例尚未初始化时会加锁,之后获取实例的性能较高。
    • 第二种方法:std::call_once 只在初始化时会有性能开销,之后获取实例的性能也较高。
  3. 代码简洁性

    • 第一种方法:代码较为复杂,需要理解和正确使用内存屏障和双重检查锁定。
    • 第二种方法:代码相对简洁,利用 std::call_once 提供的标准库特性,减少了代码复杂度。

总结

  • 推荐使用 std::call_oncestd::once_flag 的方式(第二种方法):这种方式更为简洁和安全,避免了手动管理锁和内存屏障的复杂性。标准库提供的接口已经优化了线程安全初始化的性能。
  • 在特殊情况下使用第一种方法:如果你对性能有极高的要求,或者在特定的硬件环境下需要手动优化,可以考虑使用第一种方法,但需要非常小心地处理内存屏障和线程同步问题。

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

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

相关文章

软件测试最全面试题及答案整理(2024最新版)

目录 1、你的测试职业发展是什么? 2、你认为测试人员需要具备哪些素质 3、你为什么能够做测试这一行 4、测试的目的是什么? 5、测试分为哪几个阶段? 6、单元测试的测试对象、目的、测试依据、测试方法? 7、怎样看待加班问题 8、结合你以前的学习和工作经验&#xf…

34_YOLOv5网络详解

1.1 简介 YOLOV5是YOLO&#xff08;You Only Look Once&#xff09;系列目标检测模型的一个重要版本&#xff0c;由 Ultralytics 公司的Glenn Jocher开发并维护。YOLO系列以其快速、准确的目标检测能力而闻名&#xff0c;尤其适合实时应用。YOLOV5在保持高效的同时&#xff0c…

ForCloud全栈安全体验,一站式云安全托管试用 开启全能高效攻防

对于正处于业务快速发展阶段的企业&#xff0c;特别是大型央国企而言&#xff0c;日常的安全部署和运营管理往往横跨多家子公司&#xff0c;所面临的挑战不言而喻。尤其是在面对当前常态化的大型攻防演练任务时&#xff0c;难度更是呈“几何级数”上升&#xff1a; 合规难 众…

Linux中进程的控制

一、进程的创建 1、知识储备 进程的创建要调用系统接口&#xff0c;头文件 #include<unistd.h> 函数fork() 由于之前的铺垫我们现在可以更新一个概念 进程 内核数据结构&#xff08;task_struct, mm_struct, 页表....&#xff09; 代码 数据 所以如何理解进程的独…

最新 Docker 下载镜像超时解决方案:Docker proxy

现在Docker换源也下载失败太常见了&#xff0c;至于原因&#xff0c;大家懂得都懂。本文提供一种简洁的方案&#xff0c; 利用 Docker 的http-proxy&#xff0c;代理至本机的 proxy。 文章目录 前言Docker proxy 前言 这里默认你会安装 clash&#xff0c;然后有配置和数据库。…

华为云.云日志服务LTS及其基本使用

云计算 云日志服务LTS及其基本使用 - 文章信息 - Author: 李俊才 (jcLee95) Visit me at CSDN: https://jclee95.blog.csdn.netMy WebSite&#xff1a;http://thispage.tech/Email: 291148484163.com. Shenzhen ChinaAddress of this article:https://blog.csdn.net/qq_28550…

开机出现grub无法进入系统_电脑开机出现grub解决方法

最近有小伙伴问我电脑开机出现grub无法进入系统怎么回事&#xff1f;电脑开机出grub的情况有很多&#xff0c;电脑上安装了Linux和Win10双系统&#xff0c;但是由于格式化删除了Linux之后&#xff0c;结果win10开机了之后&#xff0c;直接显示grub&#xff1e;&#xff0c;无法…

平面五杆机构运动学仿真matlab simulink

1、内容简介 略 89-可以交流、咨询、答疑 2、内容说明 略 ] 以 MATLAB 程序设计语言为平台 , 以平面可调五杆机构为主要研究对象 , 给定机构的尺寸参数 , 列出所 要分析机构的闭环矢量方程 , 使用 MATLAB 软件中 SIMULINK 仿真工具 , 在 SIMULINK 模型窗口下建立数…

UE4-光照重建

当我们拉入新的光源和模型到我们的场景中后&#xff0c;会产生这样的情况&#xff1a; Preview:预览 表示此时由于光照物体所产生的阴影都是预览级别的并不是真正的效果。 方法一&#xff1a; 或者也可以在世界大纲中选中我们的光源&#xff0c;然后将我们的光源改变为可以…

(MLLMs)多模态大模型论文分享(1)

Multimodal Large Language Models: A Survey 摘要&#xff1a;多模态语言模型的探索集成了多种数据类型&#xff0c;如图像、文本、语言、音频和其他异构性。虽然最新的大型语言模型在基于文本的任务中表现出色&#xff0c;但它们往往难以理解和处理其他数据类型。多模态模型…

“探求新质生产力 推进中国式现代化”学习交流活动在河北廊坊举办

7月21日&#xff0c;一场以“探求新质生产力 推进中国式现代化”为主题的学习交流活动在河北省廊坊市举办&#xff0c;2000余名企业界人士共同探讨企业发展的新路径与新动力。 7月21日&#xff0c;“探求新质生产力 推进中国式现代化”学习交流活动在河北省廊坊市举办。图为活动…

primeflex教学笔记20240720, FastAPI+Vue3+PrimeVue前后端分离开发

练习 先实现基本的页面结构: 代码如下: <template><div class="flex p-3 bg-gray-100 gap-3"><div class="w-20rem h-12rem bg-indigo-200 flex justify-content-center align-items-center text-white text-5xl"><input type=&q…

前端Vue组件化实践:自定义发送短信验证码弹框组件

在前端开发中&#xff0c;随着业务逻辑的日益复杂和交互需求的不断增长&#xff0c;传统的整体式开发方式逐渐暴露出效率低下、维护困难等问题。为了解决这些问题&#xff0c;组件化开发成为了一种流行的解决方案。通过组件化&#xff0c;我们可以将复杂的系统拆分成多个独立的…

JMeter:BeanShell到JSR223迁移中的注意事项

前言 在之前的文章JMeter&#xff1a;BeanShell向JSR223迁移过程遭遇的java标准库不可用问题-如何切换JDK版本中引用了一段使用BeanShell对入参进行加密的脚本&#xff0c;迁移到JSR223&#xff0c;虽然更换JDK后编译通过&#xff0c;看似也可以执行了&#xff0c;但是其实那段…

《0基础》学习Python——第二十讲__网络爬虫/<3>

一、用post请求爬取网页 同样与上一节课的get强求的内容差不多&#xff0c;即将requests.get(url,headershead)代码更换成requests.post(url,headershead),其余的即打印获取的内容&#xff0c;如果content-typejson类型的&#xff0c;打印上述代码的请求&#xff0c;则用一个命…

Linux 13:网络编程1

1. 预备知识 1-1. 理解源IP地址和目的IP地址 在IP数据包头部中&#xff0c;有两个IP地址&#xff0c;分别叫做源IP地址&#xff0c;和目的IP地址。 我们光有IP地址就可以完成通信了嘛&#xff1f;想象一下发qq消息的例子&#xff0c;有了IP地址能够把消息发送到对方的…

[嵌入式Linux]-常见编译框架与软件包组成

嵌入式常见编译框架与软件包组成 1.嵌入式开发准备工作 主芯片资料包括&#xff1a; 主芯片资料 主芯片开发参考手册&#xff1b;主芯片数据手册&#xff1b;主芯片规格书&#xff1b; 硬件参考 主芯片硬件设计参考资料&#xff1b;主芯片配套公板硬件工程&#xff1b; 软件…

Adaboost集成学习 | Matlab实现基于LSTM-Adaboost长短期记忆神经网络结合Adaboost集成学习多输入单输出时间序列预测

目录 效果一览基本介绍模型设计程序设计参考资料效果一览 基本介绍 Adaboost集成学习 | Matlab实现基于LSTM-Adaboost长短期记忆神经网络结合Adaboost集成学习时间序列预测(股票价格预测) 模型设计 步骤1: 数据准备 收集和整理历史数据。确保数据集经过适当的预处理,如归一…

自己开发软件实现网站抓取m3u8链接

几天前一个同学说想下载一个网站的视频找不到连接&#xff0c;问我有没有什么办法,网站抓取m3u8链接 网页抓取m3u8链接。当时一听觉得应该简单&#xff0c;于是说我抽空看看。然后就分析目标网页&#xff0c;试图从网页源码里找出连接&#xff0c;有的源代码直接有,但是有的没有…

【Linux学习 | 第1篇】Linux介绍+安装

文章目录 Linux1. Linux简介1.1 不同操作系统1.2 Linux系统版本 2. Linux安装2.1 安装方式2.2 网卡设置2.3 安装SSH连接工具2.4 Linux和Windows目录结构对比 Linux 1. Linux简介 1.1 不同操作系统 桌面操作系统 Windows (用户数量最多)MacOS ( 操作体验好&#xff0c;办公人…