单例模式笔记
Singleton
单例模式(Singleton Pattern)是最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要显式实例化该类的对象。
单例模式是一种创建型设计模式,它确保一个类只有一个实例,并提供了一个全局访问点来访问该实例。
注意:
- 单例类只能有一个实例。
- 单例类必须自己创建自己的唯一实例。
- 单例类必须给所有其他对象提供这一实例。
优点
- 在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例。
- 避免对资源的多重占用(比如写文件操作)。
缺点
没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。
使用场景
1、要求生产唯一序列号。
2、WEB 中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来。
3、创建的一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等。
4、Windows 是多进程多线程的,在操作一个文件的时候,就不可避免地出现多个进程或线程同时操作一个文件的现象,所以所有文件的处理必须通过唯一的实例来进行。
1. Lazy Singleton
这种方式是最基本的实现方式,这种实现最大的问题就是不支持多线程。因为没有加锁,所以严格意义上它并不算单例模式,适用于不要求线程安全的场景。
#ifndef LAZY_SINGLETON_H #define LAZY_SINGLETON_H class LazySingleton { private: static LazySingleton *instance; private: LazySingleton(); ~LazySingleton(); LazySingleton(const LazySingleton &); LazySingleton &operator=(const LazySingleton &); public: static LazySingleton *getInstance(); }; #endif
#include "LazySingleton.h" #include <iostream> LazySingleton *LazySingleton::instance = nullptr; LazySingleton *LazySingleton::getInstance() { if (instance == nullptr) { instance = new LazySingleton(); } return instance; } LazySingleton::LazySingleton() { std::cout << "LazySingleton initialized!" << std::endl; } LazySingleton::~LazySingleton() { std::cout << "LazySingleton destroyed!" << std::endl; }
上述Lazy Singleton的实现存在内存泄露的问题,有两种解决方法:
- 使用智能指针
- 使用静态的嵌套类对象
1.1 使用智能指针解决内存泄漏
#ifndef SHARED_SINGLETON_H #define SHARED_SINGLETON_H #include <memory> class SharedSingleton { private: static std::shared_ptr<SharedSingleton> instance; SharedSingleton(); ~SharedSingleton(); SharedSingleton(const SharedSingleton &); SharedSingleton &operator=(const SharedSingleton &); public: static std::shared_ptr<SharedSingleton> getInstance(); }; #endif
#include "SharedSingleton.h" #include <iostream> #include <memory> std::shared_ptr<SharedSingleton> SharedSingleton::instance = nullptr; std::shared_ptr<SharedSingleton> SharedSingleton::getInstance() { if (instance == nullptr) { instance = std::shared_ptr<SharedSingleton>( new SharedSingleton, [](SharedSingleton *obj){ delete obj; }); } return instance; } SharedSingleton::SharedSingleton() { std::cout << "SharedSingleton initialized!" << std::endl; } SharedSingleton::~SharedSingleton() { std::cout << "SharedSingleton destroyed!" << std::endl; }
1.2 使用静态嵌套类对象解决内存泄漏
#ifndef INNER_SINGLETON_H #define INNER_SINGLETON_H class InnerDeletorSingleton { private: static InnerDeletorSingleton *instance; private: InnerDeletorSingleton(); ~InnerDeletorSingleton(); InnerDeletorSingleton(const InnerDeletorSingleton &); InnerDeletorSingleton &operator=(const InnerDeletorSingleton &); private: class Deletor { public: ~Deletor() { if (InnerDeletorSingleton::instance != nullptr) delete InnerDeletorSingleton::instance; } }; static Deletor deletor; public: static InnerDeletorSingleton *getInstance(); }; #endif
#include "InnerDeletorSingleton.h" #include <iostream> InnerDeletorSingleton::Deletor InnerDeletorSingleton::deletor; InnerDeletorSingleton *InnerDeletorSingleton::instance = nullptr; InnerDeletorSingleton *InnerDeletorSingleton::getInstance() { if (instance == nullptr) { instance = new InnerDeletorSingleton(); } return instance; } InnerDeletorSingleton::InnerDeletorSingleton() { std::cout << "InnerDeletorSingleton initialized!" << std::endl; } InnerDeletorSingleton::~InnerDeletorSingleton() { std::cout << "InnerDeletorSingleton destroyed!" << std::endl; }
1.3 线程安全的Singleton方案
Lazy Singleton模式的竞争条件主要出现在第一次初始化的过程中,instance = new Singleton()
处,即可能多个线程同时检测到instance未被初始化,于是开始执行初始化工作,为了避免重复初始化,需要对这一过程上锁。
#ifndef DCL_SINGLETON_H #define DCL_SINGLETON_H #include <thread> class DCLSingleton { private: static DCLSingleton *instance; DCLSingleton(); ~DCLSingleton(); DCLSingleton(const DCLSingleton &); DCLSingleton &operator=(const DCLSingleton &); static std::mutex mutex_; public: static DCLSingleton *getInstance(); }; #endif
#include "DCLSingleton.h" #include <iostream> DCLSingleton *DCLSingleton::instance = nullptr; std::mutex DCLSingleton::mutex_; DCLSingleton *DCLSingleton::getInstance() { if (instance == nullptr) { std::lock_guard<std::mutex> lk(mutex_); if (instance == nullptr) { instance = new DCLSingleton(); } } return instance; } DCLSingleton::DCLSingleton() { std::cout << "DCLSingleton initialized!" << std::endl; } DCLSingleton::~DCLSingleton() { std::cout << "DCLSingleton destroyed!" << std::endl; }
加入DCL后,其实还是有问题的,关于memory model。
在某些内存模型中(虽然不常见)或者是由于编译器的优化以及运行时优化等等原因,使得instance虽然已经不是nullptr但是其所指对象还没有完成构造,这种情况下,另一个线程如果调用getInstance()就有可能使用到一个不完全初始化的对象。
在C++11没有出来的时候,只能靠插入两个memory barrier(内存屏障)来解决这个错误,但是C++11引进了memory model,提供了Atomic实现内存的同步访问,即不同线程总是获取对象修改前或修改后的值,无法在对象修改期间获得该对象。
C++11后就可以正确的跨平台的实现DCL模式
std::atomic<DCLSingleton *>DCLSingleton::instance = nullptr;
2. Eager Singleton
#ifndef EAGER_SINGLETON_H #define EAGER_SINGLETON_H class EagerSingleton { private: static EagerSingleton instance; EagerSingleton(); ~EagerSingleton(); EagerSingleton(const EagerSingleton &); EagerSingleton &operator=(const EagerSingleton &); public: static EagerSingleton &getInstance(); }; #endif
#include <iostream> #include "EagerSingleton.h" EagerSingleton EagerSingleton::instance; EagerSingleton &EagerSingleton::getInstance() { return instance; } EagerSingleton::EagerSingleton() { std::cout << "EagerSingleton initialized!" << std::endl; } EagerSingleton::~EagerSingleton() { std::cout << "EagerSingleton destroyed!" << std::endl; }
由于在main函数之前初始化,所以没有线程安全的问题。
但是潜在问题在于no-local static对象(函数外的static对象)在不同编译单元中的初始化顺序是未定义的。
也即,static Singleton instance;和static Singleton& getInstance()二者的初始化顺序不确定,如果在初始化完成之前调用 getInstance() 方法
会返回一个未定义的实例。
3. Meyers Singleton
C++11规定了local static在多线程条件下的初始化行为,要求编译器保证了内部静态变量的线程安全性。
#ifndef MEYER_SINGLETON_H #define MEYER_SINGLETON_H class MeyerSingleton { private: MeyerSingleton(); ~MeyerSingleton(); MeyerSingleton(const MeyerSingleton &); MeyerSingleton &operator=(const MeyerSingleton &); public: static MeyerSingleton &getInstance() { static MeyerSingleton instance; return instance; } }; #endif
#include <iostream> #include "MeyerSingleton.h" MeyerSingleton::MeyerSingleton() { std::cout << "MeyerSingleton initialized!" << std::endl; } MeyerSingleton::~MeyerSingleton() { std::cout << "MeyerSingleton destroyed!" << std::endl; }
4 总结
- Eager Singleton 虽然是线程安全的,但存在潜在问题;
- Lazy Singleton通常需要加锁来保证线程安全,但局部静态变量版本在C++11后是线程安全的;
- 局部静态变量版本(Meyers Singleton)最优雅。
5 注意事项
现有ABC三个库,其中A中封装了一个MeyerSingleton单例类,共享库B和共享库C使用A,D作为可执行程序,使用B和C,那么这个单例是否唯一?
以下是摘自chatgpt的回答:
如果A是一个共享库,封装了一个单例类,并且B和C都使用了A,那么D作为可执行程序,使用了B和C,这个单例类在整个程序中仍然是唯一的。单例类的唯一性是相对于进程而言的,因此由A封装的单例类在整个程序执行期间只会有一个实例,即使它被不同的共享库使用。
如果A作为静态库,封装了一个单例类,B和C使用A,D作为可执行程序使用B和C。由于静态库在链接时会被整合到可执行程序中,每个使用A的库和可执行程序中都将包含单例类的一个实例。因此,这个单例类在程序中仍然是唯一的,但是这个唯一性是相对于每个包含A的模块而言的,而不是整个程序。每个模块(B、C和D)都会有自己的单例实例。
此外,当libA作为静态库且在cpp文件中实现getInstance时,libB和libC同时作为动态库时出现单例不一致的问题。当libA作为动态库时,libB和libC作为动态库或者静态库时都没有这个问题 所以为了避免这个问题,最好的方式是,将getInstance的实现内联在.h文件中。
本文作者:料峭春风吹酒醒
本文链接:https://www.cnblogs.com/pengpengda/p/17991575
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步