单例模式

什么是单例模式

单例模式是一种创建型设计模式,它的核心目标是:

  • 保证一个类在整个程序生命周期内,只能有一个实例对象
  • 提供一个全局的访问点(通常是静态成员函数),让外部可以获取这个唯一实例。

在 C++ 中,实现单例模式的核心约束有两点:

  1. 禁止外部创建 / 复制对象:将类的构造函数、拷贝构造函数、赋值运算符声明为private,或直接用delete关键字禁用,防止外部通过任何方式创建多个实例;
  2. 全局访问入口:通过一个静态成员函数(如getInstance()),返回类的唯一实例(引用或指针)。

根据实例的创建时机,单例模式分为饿汉模式懒汉模式,我们先重点讲解图片中提到的饿汉模式。

饿汉模式

饿汉模式的核心特点是:程序启动时(或编译阶段)就提前创建好唯一实例,并通过静态成员变量保存下来。

核心特性

  • 优点:实现逻辑简单,且天然线程安全。 饿汉模式的实例是通过静态成员变量初始化的,在程序进入main函数之前就已完成实例创建,此时还没有用户线程启动,不存在多线程竞争创建实例的问题,因此无需额外加锁就能保证线程安全。
  • 缺点:资源可能浪费。 无论程序是否会用到这个实例,它都会在程序启动时被创建并占用资源,如果实例占用内存较大且实际未被使用,就会造成不必要的开销。

实现步骤

  • 私有化构造与拷贝构造函数:禁止外部直接通过构造、拷贝创建新对象;
  • 定义静态实例成员:在类内声明一个静态的类对象成员,用于保存唯一实例;
  • 类外初始化静态实例:在类外对静态成员变量进行初始化,程序启动时自动创建实例;
  • 提供全局访问函数:通过一个静态成员函数,返回这个静态实例的引用,让外部获取唯一对象;
  • 通过实例访问类内功能:后续所有类内函数的调用,都通过这个唯一实例完成。
#include <iostream>

class Test
{
private:
    // 1. 私有化构造函数,禁止外部直接创建对象
    Test(int a, int b)
        : _a(a), _b(b) 
    {
        std::cout << "Test instance created" << std::endl;
    }

    // 2. 禁用拷贝构造和赋值运算符,防止通过拷贝创建新实例
    Test(const Test&) = delete;
    Test& operator=(const Test&) = delete;

    // 3. 静态成员变量:保存唯一实例(饿汉模式的核心)
    static Test _instance;

    // 成员变量
    int _a;
    int _b;

public:
    // 4. 全局访问点:静态成员函数,返回唯一实例的引用
    static Test& getInstance()
    {
        return _instance;
    }

    // 类内功能函数,通过实例调用
    void printInfo()
    {
        std::cout << "a:" << _a << ",b:" << _b << std::endl;
    }
};

// 饿汉模式关键:类外初始化静态成员变量,程序启动时自动创建实例
Test Test::_instance(1, 2);

int main()
{
    // 通过静态函数获取唯一实例
    Test& t = Test::getInstance();
    
    // 调用实例的功能函数
    t.printInfo();

    // 尝试创建新实例会直接报错(验证单例的唯一性)
    // Test t2(3, 4);    // 编译报错:构造函数私有
    // Test t3 = t;      // 编译报错:拷贝构造函数已被delete

    return 0;
}

补充:饿汉模式的线程安全说明

图片中提到 “C++11 保证静态局部变量初始化是线程安全的,但此处用的是静态成员变量,同样线程安全”,这一点的原理是:

  • 静态成员变量的初始化发生在程序加载阶段,在main函数执行前完成,此时操作系统还没有调度任何用户线程,因此不存在多线程竞争的场景,是天然安全的;
  • 而懒汉模式(第一次调用getInstance()时才创建实例)在多线程场景下可能出现 “多个线程同时判断实例为空,从而创建多个对象” 的问题,需要额外通过双重检查锁等方式保证线程安全。

懒汉模式

在第一次调用 getInstance() 时才创建实例。需要处理线程安全问题,避免多线程环境下创建多个实例。

非线程安全版本:仅适用于单线程。

双重检查锁定(DCLP)版本:C++11 前需要借助 std::atomic 和内存屏障,实现较复杂。

C++11 及之后推荐方法:利用静态局部变量的线程安全特性,简洁可靠。

实现方式:

1.构造函数和拷贝构造函数必须私有化
2.类内部成员必须包含静态实例化成员的指针变量,在类外初始化
3.第一次通过get_instance返回实例时判断指针为空,加锁,随后通过new开辟空间
4.通过静态实例化成员进行访问类内的函数
 

#include <iostream>
#include <mutex>
#include <pthread.h>
 
pthread_mutex_t mtx;
 
class test
{
private:
    test(int a,int b)
    :_a(a),_b(b)
    {}
    test(const test&) = delete;
public:
    static test* get_instance()
    {
        if(_inst == nullptr)
        {
            pthread_mutex_lock(&mtx);//加锁
            if(_inst == nullptr)
                _inst = new test(1,2);
            pthread_mutex_unlock(&mtx);
        }
        return _inst;
    }
    void print()
    {
        printf("a:%d,b:%d\n",_a,_b);
    }
private:
    int _a;
    int _b;
    static test* _inst;
};
 
test* test::_inst = nullptr;
 
int main() 
{
    pthread_mutex_init(&mtx, nullptr);
    test::get_instance()->print();
    return 0;
}

生产者消费者模型

什么是生产者消费者模型

生产者消费者模型是操作系统和多线程编程中的经典同步 / 互斥模型,在 Linux 多线程场景中尤为常见。它的核心目标是:解决生产者和消费者之间 “生产 / 消费速度不匹配” 的问题,同时实现两者的解耦与安全协作。

简单来说,模型描述了两类线程如何通过一个固定大小的共享缓冲区(仓库) 协调工作,避免数据竞争、死锁或资源浪费。

模型的三个核心要素:

角色 核心职责 阻塞规则
生产者线程 负责生成数据,将数据放入缓冲区 缓冲区满时,必须停止生产并阻塞,直到消费者取走数据腾出空间
消费者线程 负责从缓冲区取出数据并处理 缓冲区空时,必须停止消费并阻塞,直到生产者放入新数据
缓冲区(仓库) 一段有限大小的共享内存(如队列、链表) 解耦生产者和消费者,允许两者以不同速度运行,实现异步协作

快速记忆:

模型的核心优势

  1. 解耦:生产者和消费者不直接通信,所有交互都通过缓冲区完成,修改一方的逻辑不会直接影响另一方。
  2. 缓冲:平衡速度差异 —— 比如生产者生产快、消费慢时,缓冲区可暂存数据;反之则避免消费者空等。
  3. 可扩展:可独立新增 / 减少生产者或消费者线程,无需修改另一方的业务逻辑。

基于阻塞队列(BlockingQueue)的实现思路

阻塞队列(BlockingQueue)是实现生产者消费者模型的常用数据结构,它的特性天然适配模型的需求。

1. 阻塞队列与普通队列的区别

阻塞队列是一种特殊的线程安全队列,核心差异在于:

  • 当队列为时,消费者线程调用pop/get操作会被阻塞,直到队列被放入新元素;
  • 当队列为时,生产者线程调用push/put操作会被阻塞,直到有元素被取出、队列腾出空间。

2. 阻塞队列如何实现模型逻辑

阻塞队列通过互斥锁 + 条件变量实现同步,核心流程如下:

生产者入队流程
  1. 加锁,访问队列;
  2. 判断队列是否已满:
    • 若已满:释放锁,等待 “队列非满” 的条件变量,直到消费者取走数据;
    • 若未满:将数据入队,唤醒等待的消费者线程,解锁。
消费者出队流程
  1. 加锁,访问队列;
  2. 判断队列是否为空:
    • 若为空:释放锁,等待 “队列非空” 的条件变量,直到生产者放入数据;
    • 若非空:取出队头数据,唤醒等待的生产者线程,解锁。

3. 关键实现细节

  • 互斥锁:保护队列的读写操作,避免多个线程同时修改队列导致数据错乱;
  • 条件变量:实现线程阻塞与唤醒,是模型同步的核心;
  • while循环判断:等待条件变量时,必须用while循环而非if,防止线程被 “虚假唤醒” 后错误操作队列。
#include <queue>
#include <mutex>
#include <condition_variable>
using namespace std;

template <typename T>
class BlockingQueue {
private:
    queue<T> _q;                  // 底层队列
    mutex _mtx;                   // 互斥锁
    condition_variable _not_full; // 队列不满的条件变量(生产者等待)
    condition_variable _not_empty;// 队列非空的条件变量(消费者等待)
    size_t _max_size;             // 缓冲区最大容量

public:
    BlockingQueue(size_t max_size) : _max_size(max_size) {}

    // 生产者:往队列中放入数据
    void push(const T& data) {
        unique_lock<mutex> lock(_mtx);
        // while循环防止虚假唤醒
        while (_q.size() >= _max_size) {
            _not_full.wait(lock); // 队列满,生产者阻塞等待
        }
        _q.push(data);
        _not_empty.notify_one(); // 唤醒等待的消费者
    }

    // 消费者:从队列中取出数据
    T pop() {
        unique_lock<mutex> lock(_mtx);
        // while循环防止虚假唤醒
        while (_q.empty()) {
            _not_empty.wait(lock); // 队列空,消费者阻塞等待
        }
        T data = _q.front();
        _q.pop();
        _not_full.notify_one(); // 唤醒等待的生产者
        return data;
    }
};

模型的典型应用场景

生产者消费者模型在实际开发中非常常见,比如:

  • 日志系统:多个业务线程作为生产者写入日志,后台线程作为消费者批量刷盘;
  • 消息队列:生产者发送消息,消费者异步处理消息;
  • 线程池:任务提交者作为生产者,工作线程作为消费者,通过任务队列解耦。
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐