线程安全的单例模式

一、什么是单例模式

现实场景类比

场景 问题 单例解决
服务器加载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;

优点 :启动快、延迟加载(省内存)

缺点线程不安全(这是重点!)


四、为什么懒汉方式"线程不安全"?

结果:两个线程各自创建了一个对象,违反了"单例"!

如果多线程调用线程池 ? 会出现什么?

  1. 内存泄漏:第一个创建的对象 B 没人引用,也无法 delete,永远占着内存

  2. 数据不一致 :不同线程拿到的是不同的线程池实例,任务投递到不同的队列,逻辑全乱

  3. 资源重复初始化:线程池里的线程、锁、条件变量都被创建了两次,系统资源耗尽


五、如何解决懒汉的线程安全问题?

加锁

复制代码
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;
}
相关推荐
charlie1145141912 小时前
通用GUI编程技术——图形渲染实战(三十六)——Constant Buffer与数据传递:CPU-GPU通信通道
开发语言·c++·windows·c·图形渲染·win32
南境十里·墨染春水2 小时前
C++笔记 STL lterator迭代器
开发语言·c++·笔记
学习使我健康2 小时前
Android 广播介绍详情
android·开发语言·kotlin
zjeweler2 小时前
宝藏网站推荐:云服务器特惠与网安学习资源的一站式聚合平台
运维·服务器·学习
lsx2024062 小时前
JavaScript Array(数组)
开发语言
小柯博客2 小时前
Amazon Kinesis Video Streams C WebRTC SDK 开发实战
c语言·开发语言·网络·stm32·嵌入式硬件·webrtc·yocto
尘世壹俗人2 小时前
如何检查服务器上消耗资源的程序是那个
服务器·前端·chrome
rannn_1112 小时前
3h速通Python:用Java的思维看懂Python
开发语言·python·ai·ai agent·大模型应用开发
时空自由民.2 小时前
Linux,ESP IDF,NuttX OS使用的项目编译管理构建体系Kconfig + Kbuild(或基于 Make/CMake 的构建系统)
linux·运维·服务器