一、什么是单例模式
现实场景类比
| 场景 | 问题 | 单例解决 |
|---|---|---|
| 服务器加载100G数据到内存 | 内存只够存一份 | 只创建一个数据管理对象 |
| 线程池、日志系统 | 多个实例会冲突/浪费资源 | 全局唯一,大家共用 |
核心思想:某些类,整个程序运行期间,只能 有且只有 一个对象(实例)存在 ---> 单例。
二、实现单例的两大难题
2.1 难题1:如何阻止用户随意创建对象?
class ThreadPool { public: ThreadPool() {} // 构造函数是 public 的 }; // 用户想创建几个就创建几个: ThreadPool tp1; // ✅ 可以 ThreadPool tp2; // ✅ 也可以 ThreadPool *tp3 = new ThreadPool(); // ✅ 还可以解决方案:把构造函数设为
private
class ThreadPool { private: ThreadPool() {} // 🔒 构造函数私有化! // 还要禁用拷贝构造和赋值(防止通过拷贝创建新对象) ThreadPool(const ThreadPool&) = delete; ThreadPool& operator=(const ThreadPool&) = delete; }; // 现在用户尝试创建: ThreadPool tp1; // ❌ 编译错误!无法访问 private 构造函数但这样又带来新问题:你自己也没法创建了!
2.2 难题2:谁来创建这个唯一的对象?
既然构造函数私有化了,对象怎么诞生?让类自己创建自己!
class ThreadPool { private: ThreadPool() {} // 私有构造 public: // 类内的静态方法 —— 这是类级别的,不需要对象就能调用 static ThreadPool* GetInstance() { // 在类内部访问私有构造函数,是合法的! return new ThreadPool(); } }; // 用户使用: ThreadPool* tp = ThreadPool::GetInstance(); // ✅ 通过静态方法创建知识点补充:静态变量 VS 全局变量
static:
static变量在程序启动时就存在于全局数据区不在任何对象的内存空间里
程序结束时才销毁
普通变量(每个对象一份)
class Student { public: int age; // 普通成员变量 }; int main() { Student s1; // s1 有自己的 age Student s2; // s2 有自己的 age s1.age = 18; s2.age = 20; // 互不影响! }静态变量(整个类只有一份)
class Student { public: static int count; // 静态成员变量 —— 所有对象共享! }; // 必须在类外初始化(这是 C++ 规则) int Student::count = 0; int main() { Student s1; Student s2; s1.count = 10; // 通过对象访问(语法上允许) cout << s2.count; // 输出 10!因为 s1 和 s2 访问的是同一个 count }三、饿汉 vs 懒汉:两种"创建时机"
理解了基本框架后,关键是什么时候创建这个唯一对象:
| 饿汉方式 | 懒汉方式 | |
|---|---|---|
| 声明位置 | static T data;(类内) | static T* inst;(类内) |
| 定义位置 | 类外T Singleton<T>::data; | 类外T* Singleton<T>::inst = nullptr; |
| 实际占用内存 | 程序启动就占用sizeof(T) | 启动只占用一个指针(8字节) |
| 对象构造时机 | 程序加载时 | 第一次调用 GetInstance() 时 |
| 线程安全 | ✅ 天然安全 | ❌ 需要手动加锁 |
3.1 饿汉方式
特点:程序启动时立即创建,"吃完饭立刻洗碗"
template <typename T> class Singleton { // 静态成员变量:程序启动时就在全局区创建好了 static T data; // ← 这里已经分配内存并构造了 public: static T* GetInstance() { return &data; // 直接返回已存在的对象地址 } }; // 必须在类外初始化静态成员(这是 C++ 规则) template<typename T> T Singleton<T>::data; // 程序加载时执行构造优点:简单、线程安全(程序启动时单线程)
缺点:启动慢(即使没用到也构造)、如果构造失败程序直接崩溃
static变量不属于任何对象,属于整个类(甚至整个程序),在全局区只有一份,程序启动时就存在。
饿汉单例就是利用这个特性:让对象在程序启动时自动建好,多线程来拿的时候只管取地址,不用抢、不用锁、不会重复创建
3.2 懒汉方式
特点:第一次用到时才创建,"吃完饭先放着,下顿要用再洗"
template <typename T> class Singleton { static T* inst; // 初始为 nullptr,还没创建 public: static T* GetInstance() { if (inst == nullptr) { // 第一次调用时判断 inst = new T(); // 🔥 这里才创建! } return inst; } }; // 类外初始化静态指针 template<typename T> T* Singleton<T>::inst = nullptr;优点:启动快、延迟加载(省内存)
缺点:线程不安全(这是重点!)
四、为什么懒汉方式"线程不安全"?
结果:两个线程各自创建了一个对象,违反了"单例"!
如果多线程调用线程池 ? 会出现什么?
内存泄漏:第一个创建的对象 B 没人引用,也无法 delete,永远占着内存
数据不一致:不同线程拿到的是不同的线程池实例,任务投递到不同的队列,逻辑全乱
资源重复初始化:线程池里的线程、锁、条件变量都被创建了两次,系统资源耗尽
五、如何解决懒汉的线程安全问题?
加锁
static ThreadPool<T>* GetInstance() { LockGuard lockguard(_lock); //加锁 每次调用都加锁! if (inc == nullptr) { inc = new ThreadPool<T>(); inc->Start(); } return inc; } // 解锁
问题:单例已经存在了,但每次还要排队加锁!100个线程调用 = 100次串行排队,性能极差!
双层if(DCL)的完美解决
static ThreadPool<T>* GetInstance() { // 【第一层 if】无锁快速通道 —— 99% 的情况走这里 if (inc == nullptr) // ⭐ 无锁检查! { LockGuard lockguard(_lock); // 🔒 只有首次才加锁 // 【第二层 if】有锁安全通道 —— 防止排队线程重复创建 if (inc == nullptr) // ⭐ 再检查一次! { inc = new ThreadPool<T>(); inc->Start(); } } // 🔓 return inc; }场景1:单例已创建(99% 调用)
场景2:首次创建(仅1次)
只有线程A创建对象,B和C拿到锁后发现已经存在,直接返回。对象唯一,无泄漏,无重复创建
六、单例模式修改V1版本的多线程
Linux线程同步与互斥(五):线程池的全面实现-CSDN博客
#pragma once #include <iostream> #include <string> #include "Log.hpp" #include <vector> #include <queue> #include "Cond.hpp" #include " Thread.hpp" namespace ThreadPoolModule { using namespace ThreadModlue; using namespace LogModule; using namespace CondModule; using namespace MutexModule; static const int gnum = 4; template <typename T> class ThreadPool { private: void WakeUpAllThread() { LockGuard localguard(_mutex); if (_sleepernum) _cond.Broadcast(); LOG(LogLevel::INFO) << "唤醒所有的休眠的线程"; } void WakeUpOne() { _cond.Signal(); LOG(LogLevel::INFO) << "唤醒一个的休眠的线程"; } ThreadPool(int num = gnum) : _num(num), _isrunning(false), _sleepernum(0) { for (int i = 0; i <= num; i++) { _threads.emplace_back( [this]() { HandlerTask(); }); } } ThreadPool(const ThreadPool<T> &) = delete; ThreadPool<T> &operator=(const ThreadPool<T> &) = delete; public: static ThreadPool<T> *GetInstance() { if (inc == nullptr) { LockGuard lockguard(_lock); LOG(LogLevel::DEBUG) << "获取单例...."; if (inc == nullptr) { LOG(LogLevel::DEBUG) << "首次使用单例,创建之...."; inc = new ThreadPool<T>(); inc->Start(); } } return inc; } void Start() { if (_isrunning) return; // 如果线程已经启动了,返回 _isrunning = true; for (auto &thread : _threads) { thread.Start(); LOG(LogLevel::INFO) << "create new thread success: " << thread.Name(); } } void Stop() { if (!_isrunning) return; _isrunning = false; // 唤醒所有线程 WakeUpAllThread(); } void Join() { for (auto &thread : _threads) { thread.Join(); } } void HandlerTask() { char name[128]; pthread_getname_np(pthread_self(), name, sizeof(name)); while (true) { T t; { LockGuard lockguard(_mutex); // 1.a.队列是否为空 b.线程池没有退出 while (_taskq.empty() && _isrunning) { _sleepernum++; _cond.Wait(_mutex); _sleepernum--; } // 2.内部的线程被唤醒 if (!_isrunning && _taskq.empty()) { LOG(LogLevel::INFO) << name << "退出了,线程池退出&&任务队列为空"; break; } // 一定有任务 t = _taskq.front(); // 从q中获取任务,任务已经是线程私有的了 _taskq.pop(); } t(); // 处理任务,需要在临界区内部处理吗? } } bool Enqueue(const T &in) { if (_isrunning) { LockGuard lockguard(_mutex); _taskq.push(in); if (_threads.size() - _sleepernum == 0) WakeUpOne(); return true; } return false; } ~ThreadPool() {}; private: std::vector<Thread> _threads; int _num; // 线程池中,线程的个数 std::queue<T> _taskq; Cond _cond; Mutex _mutex; bool _isrunning; int _sleepernum; // bug?? static ThreadPool<T> *inc; // 单例指针 static Mutex _lock; }; template <typename T> ThreadPool<T> *ThreadPool<T>::inc = nullptr; template <typename T> Mutex ThreadPool<T>::_lock; }