设计模式学习之——单例模式

文章目录

单例模式

什么叫做单例模式

顾名思义,简单来说,单例模式就是只有一个用例的意思
那么官方一点解释是什么:

单例模式是一种设计模式,它确保某个类只有一个实例,并提供一个全局访问点来获取该实例 。这种模式在多种编程语言中都有实现,包扩Java和C++。单例模式的实现可以采取饿汉式懒汉式两种方式。**饿汉式是在类加载时就创建了实例,而懒汉式则是在首次使用时才创建实例。**懒汉式在多线程环境下可能会遇到线程安全问题,需要额外的线程安全措施来保证单例对象的唯一性

单例模式的动机

这里有一个问题:Windows的任务管理器无论启动多少次,为什么始终只能弹出一个任务管理器的窗口呢?也就是说在一个Windows系统中,任务管理器存在唯一性,为什么要这样设计呢?

可以从以下两个方面来分析:

  • 其一,如果能弹出多个窗口,且这些窗口的内容完全一致,全部是重复对象,这势必会浪费系统资源(任务管理器需要获取系统运行时的诸多信息,这些信息的获取需要消耗一定的系统资源,包括CPU资源及内存资源等),而且根本没有必要显示多个内容完全相同的窗口;
  • 其二,如果弹出的多个窗口内容不一致,问题就更加严重了,这意味着在某一瞬间系统资源使用情况和进程、服务等信息存在多个状态,例如任务管理器窗口A显示"CPU使用率"为10%,窗口B显示"CPU使用率"为15%,到底哪个才是真实的呢?这会给用户带来误解,更不可取。

在实际开发中也经常遇到类似的情况,为了节约系统资源,有时需要确保系统中某个类只有唯一一个实例当这个唯一实例创建成功之后,无法再创建一个同类型的其他对象,所有的操作都只能基于这个唯一实例。为了确保对象的唯一性,可以通过单例模式来实现,这就是单例模式的动机所在。

简单单例模式

模拟实现一下Windows任务管理器

为了实现WIndows任务管理器的唯一性,通过以下三步对TaskManager类进行重构

  1. 由于每次使用new关键字来实例化TaskManager类时都将产生一个新对象,为了确保TaskManager实例的唯一性,需要禁止类的外部直接使用new来创建对象,因此需要将TaskManager的构造函数的可见性改为private,代码如下:

  2. 将构造函数的可见性改为private后,虽然类的外部不能再使用new来创建对象,但是在TaskManager的内部还是可以创建对象的,可见性只对类外有效。因此,可以在TaskManager中创建并保存这个唯一实例。为了让外界可以访问这个唯一实例,需要在TaskManager中定义一个静态的TaskManager类型的私有成员变量,代码如下:

  3. 为了保证成员变量的封装性,将TaskManager类型的tm对象的可见性设置为private,但外界该如何使用该成员变量并何时实例化该成员变量呢?答案是增加一个公有的静态方法,代码如下:

在getInstance()方法中首先判断tm对象是否存在,如果不存在(即tm==null为true),则使用new关键字创建一个新的TaskManager类型的tm对象,再返回新创建的tm对象;否则直接返回已有的tm对象。

需要注意的是getInstance()方法的修饰符,首先它应该是一个public方法,以便外界其他对象使用;其次它使用了static关键字,即它是一个静态方法,在类外可以直接通过类名来访问,而无须创建TaskManager对象。事实上,在类外也无法创建TaskManager对象,因为构造函数是私有的。

思考

为什么要将成员变量tm定义为静态变量?

通过以上三个步骤,完成了一个个最简单的单例类的设计,其完整代码如下:

cpp 复制代码
class TaskManager{
    private:
    TaskManager(){}//初始化窗口
    public:
    void displayProcess(){}//显示进程
    void displayServices(){}//显示服务
    public:
    static TaskManager* getInstance(){
        if(tm == nullptr)
        {
            tm = new TaskManager();
        }
        return tm;
    }
    private:
    static TaskManager *tm;
};
TaskManager * TaskManager::tm = nullptr;

在类外无法直接创建新的TaskManager对象,但可以通过代码TaskManager::getInstance()访问实例对象。第一次调用getInstance()方法时将创建唯一实例,再次调用时将返回第一次创建的实例。

饿汉式单例和懒汉式单例

上面的简单的单例模式存在一个问题,在多线程的场景下,++当第一次调用getInstanc()时创建并对象时,tm为nullptr值,因此系统将执行代码 tm = new Taskanager();在这个过程中,如果初始化工作要进行大量工作,则需要一段时间来创建TaskManager对象。而在此时,如果再一次调用getInstance()方法(通常发生在多线程环境中),由于TaskManager对象尚未创建成功,仍为nullptr值,判断条件"tm == nullptr"为真值,因此代码 tm = new TaskManager();会再次执行,导致最终创建了多个tm对象,这违背了单例模式的初衷,也可能会导致发生运行错误。++

那么如何解决这个问题呢?

饿汉式单例

从图中可以看到当类被加载时,静态变量instance会被初始化,此时类的私有构造函数会被调用,单例类的唯一实例将被创建,可确保单例对象的唯一性。

懒汉式单例

在前面的简单单例模式的介绍中,我们用的就是懒汉式的单例模式,但是在多线程的场景中会出现问题,在 C++11 中,静态局部变量这种方式天然是线程安全的,不存在线程不安全的问题。原因是C++ 11标准中新增了一个特性叫Magic Static:如果变量在初始化时,并发线程同时进入到static声明语句,并发线程会阻塞等待初始化结束。

但是为了方便学习我们还是需要在这里引入互斥锁

cpp 复制代码
class LazySingleton
{
private:
    LazySingleton() {}
    ~LazySingleton() {}

private:
    static LazySingleton *instance;
    static std::mutex mutex; // 互斥锁
public:
    static LazySingleton *getInstance()
    {
        if (instance == nullptr)
        {
            std::lock_guard<std::mutex> lock(mutex); // 加锁

            if (instance == nullptr)
            {
                instance = new LazySingleton();
            }
        }
        return instance;
    }
};
LazySingleton* LazySingleton::instance = nullptr;

单例模式总结

单例模式作为一种目标明确、结构简单、理解容易的设计模式,在软件开发中使用频率相当高,在很多应用软件和框架中都得以广泛应用。

1.主要优点

单例模式的主要优点如下:

(1)单例模式提供了对唯一实例的受控访问。因为单例类封装了它的唯一实例,所以它可以严格控制客户怎样以及何时访问它。

(2)由于在系统内存中只存在一个对象,因此可以节约系统资源。对于一些需要频繁创建和销毁的对象,单例模式无疑可以提高系统的性能。

(3)允许可变数目的实例。基于单例模式,开发人员可以进行扩展,使用与控制单例对象相似的方法来获得指定个数的实例对象,既节省系统资源,又解决了由于单例对象共享过多有损性能的问题。(注:自行提供指定数目实例对象的类可称之为多例类。)

2.主要缺点

单例模式的主要缺点如下:

(1)由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。

(2)单例类的职责过重,在一定程度上违背了单一职责原则。因为单例类既提供了业务方法,又提供了创建对象的方法(工厂方法),将对象的创建和对象本身的功能耦合在一起。

(3)现在很多面向对象语言(如Java、C#)的运行环境都提供了自动垃圾回收技术,因此,如果实例化的共享对象长时间不被利用,系统会认为它是垃圾,会自动销毁并回收资源,下次利用时又将重新实例化,这将导致共享的单例对象状态的丢失。

3.适用场景

在以下情况下可以考虑使用单例模式:

(1)系统只需要一个实例对象。例如,系统要求提供一个唯一的序列号生成器或资源管理器,或者需要考虑资源消耗太大而只允许创建一个对象。

(2)客户调用类的单个实例只允许使用一个公共访问点。除了该公共访问点,不能通过其他途径访问该实例。

相关推荐
使者大牙1 分钟前
【大语言模型学习笔记】第一篇:LLM大规模语言模型介绍
笔记·学习·语言模型
As977_23 分钟前
前端学习Day12 CSS盒子的定位(相对定位篇“附练习”)
前端·css·学习
ajsbxi26 分钟前
苍穹外卖学习记录
java·笔记·后端·学习·nginx·spring·servlet
Rattenking27 分钟前
React 源码学习01 ---- React.Children.map 的实现与应用
javascript·学习·react.js
dsywws1 小时前
Linux学习笔记之时间日期和查找和解压缩指令
linux·笔记·学习
道法自然04021 小时前
Ethernet 系列(8)-- 基础学习::ARP
网络·学习·智能路由器
爱吃生蚝的于勒1 小时前
深入学习指针(5)!!!!!!!!!!!!!!!
c语言·开发语言·数据结构·学习·计算机网络·算法
wrx繁星点点1 小时前
状态模式(State Pattern)详解
java·开发语言·ui·设计模式·状态模式
cuisidong19973 小时前
5G学习笔记三之物理层、数据链路层、RRC层协议
笔记·学习·5g
南宫理的日知录3 小时前
99、Python并发编程:多线程的问题、临界资源以及同步机制
开发语言·python·学习·编程学习