【单例模式】
概述
一个类不管创建多少次对象,永远只能得到该类型的一个对象的实例。
常用到的比如日志模块 ,数据库模块
饿汉:在类加载时就创建单例对象,因此它是线程安全的,因为对象的创建在程序启动时就已经完成,不存在多线程同时创建对象的问题。
懒汉:在第一次使用单例对象时才创建对象,这种方式在多线程环境下需要考虑线程安全问题,通常使用互斥锁来保证对象只被创建一次。
1. 一个私有构造函数(确保只能单例类自己创建实例):
单例类通常会将其构造函数设为私有,以防止外部代码直接实例化对象。
2. 一个私有静态变量(确保只有一个实例)
单例类通常包含一个私有的静态变量,用于保存该类的唯一实例。
3. 一个公有静态函数(给使用者提供调用方法)
单例模式的6种实现
1、懒汉式(线程不安全)
class Singleton {
private:// 延迟初始化:初始为 nullptr,第一次使用时创建static Singleton* instance;// 私有构造:禁止外部 newSingleton() {}// 禁用拷贝(防止复制实例)Singleton(const Singleton&) = delete;Singleton& operator=(const Singleton&) = delete;public:// 全局访问点static Singleton* getInstance() {if (instance == nullptr) {instance = new Singleton(); // 第一次调用时创建实例}return instance;}
};// 静态成员类外初始化
Singleton* Singleton::instance = nullptr;
先不创建实例,当第一次被调用时,再创建实例,所以被称为懒汉式。
优点:延迟了实例化,如果不需要使用该类,就不会被实例化,只有在需要时才创建实例,避免了资源浪费。
缺点:线程不安全,多线程环境下,如果多个线程同时进入了` if (instance == null) `,若此时还未实例化,也就是`instance == null`,那么就会有多个线程执行 `instance = new Singleton(); `,就会实例化多个实例;
2、饿汉式(线程安全)
class Singleton {
private:static Singleton instance;Singleton() {}Singleton(const Singleton&) = delete;Singleton& operator=(const Singleton&) = delete;public:static Singleton& getInstance() {return instance;}
};Singleton Singleton::instance; // 静态成员类外初始化(程序启动时执行)
先不管需不需要使用这个实例,直接先实例化好实例(饿死鬼一样,所以称为饿汉式),然后当需要使用的时候,直接调方法就可以使用了
优点:提前实例化好了一个实例,避免了线程不安全问题的出现,
缺点:直接实例化了实例,不再延迟实例化;若系统没有使用这个实例,或者系统运行很久之后才需要使用这个实例,都会使操作系统的资源浪费。
3、懒汉式(线程安全)
class Singleton {
private:static Singleton* instance;static std::mutex mtx; // 互斥锁:保证线程安全Singleton() {}Singleton(const Singleton&) = delete;Singleton& operator=(const Singleton&) = delete;public:// 整个方法加锁:每次调用都需获取锁static Singleton* getInstance() {std::lock_guard<std::mutex> lock(mtx); // 自动加锁/解锁,避免死锁if (instance == nullptr) {instance = new Singleton();}return instance;}
};// 静态成员初始化
Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mtx;
实现和线程不安全的懒汉式 几乎一样,唯一不同的点是,在get方法上 加了一把锁。如此一来,多个线程访问,每次只有拿到锁的的线程能够进入该方法,避免了多线程不安全问题的出现。
优点:延迟实例化,节约了资源,并且是线程安全的。
缺点:虽然解决了线程安全问题,但是性能降低了。因为,即使实例已经实例化了,既后续不会再出现线程安全问题了,但是锁还在,每次还是只能拿到锁的线程进入该方法使线程阻塞,等待时间过长。
4、双重检查锁实现(线程安全)
class Singleton {
private:// volatile确保多线程下实例状态可见static volatile Singleton* instance;static std::mutex mtx;Singleton() {}Singleton(const Singleton&) = delete;Singleton& operator=(const Singleton&) = delete;public:// 双重检查锁定实现线程安全的懒加载static Singleton* getInstance() {// 第一次检查:避免频繁加锁if (instance == nullptr) {// 加锁:确保只有一个线程进入初始化std::lock_guard<std::mutex> lock(mtx);// 第二次检查:防止多线程同时通过第一次检查if (instance == nullptr) {instance = new Singleton();}}return const_cast<Singleton*>(uniqueInstance);}
};// 静态成员初始化
volatile Singleton* Singleton::uniqueInstance = nullptr;
std::mutex Singleton::mtx;
双重检查锁相当于是改进了线程安全的懒汉式。线程安全的懒汉式的缺点是性能降低了,造成的原因是因为即使实例已经实例化,依然每次都会有锁。
而现在,我们将锁的位置变了,并且多加了一个检查。也就是,先判断实例是否已经存在,若已经存在了,则不会执行判断方法内的有锁方法了。 而如果,还没有实例化的时候,多个线程进去了,也没有事,因为里面的方法有锁,只会让一个线程进入最内层方法并实例化实例。如此一来,最多最多,也就是第一次实例化的时候,会有线程阻塞的情况,后续便不会再有线程阻塞的问题。
volatile 的作用
`new Singleton()` 底层分 3 步:分配对象 -> 初始化对象 -> 指针指向内存
编译器/CPU可能重排上述顺序,1->3->2,导致B拿到未初始化的实例,`volatile` 禁止重排,确保“初始化完成后才能赋值指针”,同时保证对线程对`instance`的读写可见
5、静态内部类的实现
class Singleton {
private:// 私有构造Singleton() {}// 禁用拷贝Singleton(const Singleton&) = delete;Singleton& operator=(const Singleton&) = delete;// 静态内部类:仅在被访问时加载static class SingletonHolder {public:// 内部类静态成员:程序启动时初始化(线程安全)static Singleton instance;};public:// 全局访问点:调用时触发内部类加载static Singleton& getInstance() {return SingletonHolder::instance;}
};// 静态内部类的静态成员初始化
Singleton Singleton::SingletonHolder::instance;
延迟加载:外部类 `Singleton` 加载时,内部类 `SingletonHolder` 不加载;仅调用 `getInstance()` 时,内部类才加载并初始化 `instance`。
线程安全:C++ 静态成员初始化在单线程阶段执行,天然避免并发问题
缺点:无法主动销毁实例(实例随程序退出释放)
6、枚举类(线程安全 )
// 枚举类:默认线程安全,且天然防止拷贝
enum class Singleton {INSTANCE; // 唯一枚举实例// 枚举类成员方法(扩展功能)void doSomething() {std::cout << "Singleton 执行任务" << std::endl;}
};// 全局访问宏(可选,简化调用)
#define SINGLETON Singleton::INSTANCE
枚举类的三个特点:
1. 成员唯一:枚举里的每个成员(INSTANCE)是全局唯一的,整个程序只有一个,不能像普通类那样new多个对现象
2. 禁止拷贝
3. 自动初始化:枚举的成员会在程序启动时自动初始化(饿汉),无需手动创建
使用场景
(1)频繁实例化然后又销毁的对象,使用单例模式可以提高性能
(2)经常使用的对象,但实例化时耗费时间或者资源多,如数据库连接池,使用单例模式,可以提高性能,降低资源损坏
(3)使用线程池之类的控制资源时,使用单例模式,可以方便资源之间的通信
具体如:日志,数据库连接池,计数器等