设计模式 - 创建型模式 - 单例模式(C++)
2022-06-15 23:16
8047 查看
1、前言
单例模式属于创建型模式,保证一个类仅有一个实例,并提供一个访问它的全局访问点。
单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。
2、介绍
2.1、主要解决
防止一个系统全局使用的类频繁地创建与销毁、解决多线程并发访问的问题和节约系统内存等,提高系统运行的效率,提高系统性能。
什么情况需要使用全局的类?通常是对共享资源的使用。
比如需要实现系统控制打印机工作,一般都会定义一个“打印机管理类”用来管理打印机的各个功能,有多个模块都需要控制打印机工作,在没有使用单例模式的情况下,会遇到一下问题:
- 模块会在需要打印时创建“打印机管理类”,打印完成即销毁,如果需要频繁打印,那么就会频繁地创建和销毁该类;
- 如果打印完成不销毁呢?那么多个模块都会创建“打印机管理类”,浪费系统资源;
- 一旦出现多个模块同时需要打印,就要解决各模块之间的打印同步问题,毕竟打印机只有一个。
使用单例模式后:
- 单例模式会自行创建,同时只提供一个全局访问的类,因此不会造成系统资源的浪费,因此也没有了频繁地创建和销毁的需求。
- 多个模块同时需要打印时,只需要由全局的“打印机管理类”实例对打印的任务进行同步管理,无需多个模块之间解决同步的问题,提高系统运行的效率。
2.2、优缺点
单例模式的使用需要根据实际情况使用,以下是该设计模式的优缺点。
优点:
- 在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例;
- 避免了多线程并发访问时只需要该类管理同步即可,提高系统运行的效率,提高系统性能。
缺点:
- 没有接口,不能继承,与单一职责原则冲突;
- 单例模式可能掩盖不良设计, 比如程序各组件之间相互了解过多等。
3、实现
在了解单例模式可以解决什么问题的情况下,那么如何实现一个单例模式呢?
- 在类中添加一个该类的私有静态成员变量用于保存单例实例。
- 声明一个公有静态构建方法用于获取单例实例。
- 将类的构造函数设为私有。
3.1、懒汉设计
顾名思义,不到万不得已就不会去实例化类,对象只有被调用时才去创建,这种方式是最基本的实现方式,但这种实现最大的问题就是不支持多线程,属于线程不安全的,在多线程使用中,容易多次创建实例,因此严格的来说,并不算单例模式。
class CPrinter { public: static CPrinter *GetInstance() // 提供全局访问实例节点 { if (ms_pinstance == nullptr) { ms_pinstance = new CPrinter(); } return ms_pinstance; } private: CPrinter(){} ~CPrinter(){} private: static CPrinter *ms_pinstance; public: void Work(void) { printf("[%p]Printer working\n", this); } }; CPrinter *CPrinter::ms_pinstance = nullptr; int main() { CPrinter::GetInstance()->Work(); return 0; }
为了解决在多线程使用中,容易多次创建实例的问题,可以加上互斥锁,这样就保证了线程安全。
#include <iostream> #include <thread> #include <mutex> using namespace std; class CPrinter { public: static CPrinter *GetInstance() // 提供全局访问实例节点 { ms_initMutex.lock(); if (ms_pinstance == nullptr) { ms_pinstance = new CPrinter(); } ms_initMutex.unlock(); return ms_pinstance; } private: CPrinter(){} ~CPrinter(){} private: static CPrinter *ms_pinstance; static std::mutex ms_initMutex; public: void Work(void) { printf("[%p]Printer working\n", this); } }; // 类外初始化 CPrinter *CPrinter::ms_pinstance = nullptr; std::mutex CPrinter::ms_initMutex; int main() { /* 创建线程1 */ std::thread([=]{ CPrinter::GetInstance()->Work(); }).detach(); /* 创建线程2 */ std::thread([=]{ CPrinter::GetInstance()->Work(); }).detach(); return 0; }
3.2、懒汉设计+双重校验锁
虽然上述的优化后的懒汉设计实现解决了线程安全的问题,但加锁会影响效率,为了解决线程安全的问题,同时提高效率,采用双重检查加锁机制,先判断是否为 nullptr,如果不为空,则直接返回实例,避免加锁影响效率,而加锁之后再判断,是为了防止在加锁之前多个线程都进入且竞争互斥锁,避免下一个线程获取锁后再次创建实例。
#include <iostream> #include <thread> #include <mutex> using namespace std; class CPrinter { public: static CPrinter *GetInstance() // 提供全局访问实例节点 { if (ms_pinstance == nullptr) { ms_initMutex.lock(); // 如果为空则加锁 if (ms_pinstance == nullptr) { ms_pinstance = new CPrinter(); } ms_initMutex.unlock(); } return ms_pinstance; } private: CPrinter(){} ~CPrinter(){} private: static CPrinter *ms_pinstance; static std::mutex ms_initMutex; public: void Work(void) { printf("[%p]Printer working\n", this); } }; // 类外初始化 CPrinter *CPrinter::ms_pinstance = nullptr; std::mutex CPrinter::ms_initMutex; int main() { /* 创建线程1 */ std::thread([=]{ CPrinter::GetInstance()->Work(); }).detach(); /* 创建线程2 */ std::thread([=]{ CPrinter::GetInstance()->Work(); }).detach(); return 0; }
3.3、饿汉设计
饿汉模式就是在单例类定义的时候(即在main函数之前)就进行实例化。因为main函数执行之前,全局作用域的类成员静态变量ms_instance 已经初始化,因此就不存在多线程的问题。
#include <iostream> #include <thread> using namespace std; class CPrinter { public: static CPrinter *GetInstance() // 提供全局访问实例节点 { return &ms_instance; } private: CPrinter(){} ~CPrinter(){} private: static CPrinter ms_instance; public: void Work(void) { printf("[%p]Printer working\n", this); } }; CPrinter CPrinter::ms_instance; int main() { std::thread([=]{ CPrinter::GetInstance()->Work(); }).detach(); std::thread([=]{ CPrinter::GetInstance()->Work(); }).detach(); return 0; }