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

文章目录

单例模式

什么叫做单例模式

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

单例模式是一种设计模式,它确保某个类只有一个实例,并提供一个全局访问点来获取该实例 。这种模式在多种编程语言中都有实现,包扩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)客户调用类的单个实例只允许使用一个公共访问点。除了该公共访问点,不能通过其他途径访问该实例。

相关推荐
南宫生30 分钟前
力扣-图论-17【算法学习day.67】
java·学习·算法·leetcode·图论
sanguine__1 小时前
Web APIs学习 (操作DOM BOM)
学习
数据的世界013 小时前
.NET开发人员学习书籍推荐
学习·.net
四口鲸鱼爱吃盐3 小时前
CVPR2024 | 通过集成渐近正态分布学习实现强可迁移对抗攻击
学习
OopspoO5 小时前
qcow2镜像大小压缩
学习·性能优化
A懿轩A6 小时前
C/C++ 数据结构与算法【栈和队列】 栈+队列详细解析【日常学习,考研必备】带图+详细代码
c语言·数据结构·c++·学习·考研·算法·栈和队列
居居飒6 小时前
Android学习(四)-Kotlin编程语言-for循环
android·学习·kotlin
思忖小下6 小时前
梳理你的思路(从OOP到架构设计)_简介设计模式
设计模式·架构·eit
kkflash36 小时前
提升专业素养的实用指南
学习·职场和发展
1 9 J7 小时前
数据结构 C/C++(实验五:图)
c语言·数据结构·c++·学习·算法