一、什么是单例模式
现实场景类比
| 场景 | 问题 | 单例解决 |
|---|---|---|
| 服务器加载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;
}



