news 2026/4/26 18:55:29

线程安全的单例模式

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
线程安全的单例模式

一、什么是单例模式

现实场景类比

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/26 18:45:34

OpenClaw System Prompt 安全规则(Safety)源码分析

OpenClaw System Prompt 安全规则(Safety)源码分析 核心结论 Safety 部分是代码中硬编码的,没有任何外部配置或用户可覆盖机制。 Safety 位于 AGENTS.md(Project Context)之前,属于不可绕过的核心约束层。即使用户在 AGENTS.md 中写入"无需遵守安全规则",该…

作者头像 李华
网站建设 2026/4/26 18:37:30

老王-与辉同行:直播带货进入“人心时代”的里程碑

与辉同行&#xff1a;直播带货进入“人心时代”的里程碑“流量留不住人心&#xff0c;人心自有真情相伴。”一、数据背后的时代转折 首秀战绩&#xff08;2023年12月9日后一个月&#xff09;&#xff1a; 3小时涨粉300万 → 平均每分钟1.6万人销售额1.5亿元点赞量12.9亿峰值在线…

作者头像 李华
网站建设 2026/4/26 18:30:39

DeepEval全景解析:构建企业级LLM评估体系的战略转型指南

DeepEval全景解析&#xff1a;构建企业级LLM评估体系的战略转型指南 【免费下载链接】deepeval The LLM Evaluation Framework 项目地址: https://gitcode.com/GitHub_Trending/de/deepeval 在人工智能技术快速迭代的今天&#xff0c;大型语言模型&#xff08;LLM&#…

作者头像 李华
网站建设 2026/4/26 18:25:22

B站缓存视频合并神器:让碎片化缓存秒变完整MP4

B站缓存视频合并神器&#xff1a;让碎片化缓存秒变完整MP4 【免费下载链接】BilibiliCacheVideoMerge &#x1f525;&#x1f525;Android上将bilibili缓存视频合并导出为mp4&#xff0c;支持安卓5.0 ~ 13&#xff0c;视频挂载弹幕播放(Android consolidates and exports the b…

作者头像 李华