news 2026/4/21 17:15:41

单例模式 | 死锁

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
单例模式 | 死锁

单例模式

什么是单例模式?

单例模式是一种创建型设计模式,它保证一个类只有一个实例,并提供一个全局访问点。就像 一个国家只有一个总统 。

核心特点

  • 唯一性 :内存中只能有一个对象。
  • 全局访问 :任何地方都可以通过 GetInstance () 访问它。
  • 私有构造 :构造函数必须私有 (private),防止外部 new 。
  • 禁拷贝 :拷贝构造和赋值运算符必须禁用 (delete)。

现在我们要对下面这个很大的类做一个单例模式

class BigData { public: BigData() { std::cout << " [BigData] 构造函数被调用 (加载100GB数据...)" << std::endl; } void process() { std::cout << " [BigData] 正在处理数据..." << std::endl; } };

饿汉模式

  • 原理 : “饿了就要吃” 。程序一启动( main 函数执行前),就立马把对象创建好。
class SingletonEager { private: static BigData data; // 静态成员对象 SingletonEager() {} // 私有构造函数,禁止外部创建 SingletonEager(const SingletonEager&) = delete; SingletonEager& operator=(const SingletonEager&) = delete; public: static BigData* GetInstance() { return &data; } }; // 在类外初始化静态成员 BigData SingletonEager::data;
  • 优点 :
    • 天然线程安全 :C++ 保证静态变量在 main 之前初始化,此时还没多线程。
    • 执行效率高 :获取实例时不需要加锁判断。
  • 缺点 :
    • 启动慢 :如果对象很大(加载 100G 数据),程序启动会卡很久。
    • 浪费内存 :如果程序运行了一整天都没用到它,这 100G 内存就白占了。

懒汉模式 - 线程不安全

  • 原理 : “懒得动,要用才去洗碗” 。第一次调用 GetInstance 时才创建。
  • 问题 :多线程环境下,A 线程判断 inst == NULL 准备创建,还没创建完,B 线程也判断 inst == NULL ,于是两人都创建了一份。
class SingletonLazyUnsafe { private: static BigData* inst; SingletonLazyUnsafe() {} SingletonLazyUnsafe(const SingletonLazyUnsafe&) = delete; SingletonLazyUnsafe& operator=(const SingletonLazyUnsafe&) = delete; public: static BigData* GetInstance() { if (inst == NULL) { inst = new BigData(); } return inst; } }; BigData* SingletonLazyUnsafe::inst = NULL;

懒汉模式 - 线程安全版

这是经典的生产级实现

class SingletonLazySafe { private: // volatile: 防止编译器对代码进行过度优化(例如指令重排), // 确保多线程下 inst 的可见性和有序性。 volatile static BigData* inst; static std::mutex _mtx; SingletonLazySafe() {} SingletonLazySafe(const SingletonLazySafe&) = delete; SingletonLazySafe& operator=(const SingletonLazySafe&) = delete; public: static BigData* GetInstance() { // 第一重检查:如果已经创建了,直接返回,避免每次都加锁(性能关键!) if (inst == NULL) { _mtx.lock(); // 加锁 // 第二重检查:防止在加锁等待期间,别人已经创建了 if (inst == NULL) { inst = new BigData(); } _mtx.unlock(); // 解锁 } return (BigData*)inst; } }; volatile BigData* SingletonLazySafe::inst = NULL; std::mutex SingletonLazySafe::_mtx;
  • 注意点 :
    1. 双重 if :外层 if 挡住 99% 的请求(避免锁竞争),内层 if 保证安全性。
    2. volatile :volatile static T* inst;防止编译器优化指令重排(在某些老旧编译器或特定硬件上, new 操作可能被乱序,导致返回未完全构造的对象)。

Meyers' Singleton

如果你用的是 C++11 及以上,这是最推荐的写法。

class SingletonMeyers { private: SingletonMeyers() {} public: static BigData& GetInstance() { // C++11 规定:局部静态变量的初始化是线程安全的 static BigData instance; return instance; } };
  • 原理 :C++11 标准明确规定:局部静态变量的初始化是线程安全的。编译器会自动加锁保护初始化过程。
  • 优点 :代码极少,既是懒汉(第一次调用才初始化),又是线程安全的,还没指针管理的麻烦。

总结建议

模式启动速度运行时性能线程安全推荐指数
饿汉快 (无锁)⭐⭐ (仅限小对象)
懒汉 (不安全)❌ (禁止使用)
懒汉 (DCL)中 (首次加锁)⭐⭐⭐ (旧标准 / 复杂控制)
Meyers (局部静态)⭐⭐⭐⭐⭐ (C++11 首选)

关于volatile

你可能以为inst = new T();是一个原子操作(要么做完,要么没做),但实际上,编译器会把它拆成三步独立的指令

// 伪代码:new T() 的实际执行步骤 1. 分配内存:给 T 类型的对象申请一块内存空间(比如 malloc ); 2. 初始化对象:调用 T 的构造函数,给这块内存赋值(比如初始化成员变量); 3. 指针赋值:把 inst 指针指向刚分配的内存地址。

正常情况下,CPU 按「1→2→3」执行,inst 只有在对象完全构造好后才会非 NULL,这没问题。

但是为了提升执行效率,编译器(或 CPU)会对没有数据依赖的指令做「指令重排」(这就是 “过度优化” 的核心)。

对于上面的三步,编译器会认为:“步骤 2(初始化对象)和步骤 3(指针赋值)没有直接依赖”,于是可能把顺序改成:

1. 分配内存 → 3. 指针赋值 → 2. 初始化对象

这个重排对单线程完全无害,但对「多线程的 DCL 场景」是致命的!

假设现在有线程 A 和线程 B,执行流程如下:

  1. 线程 A 执行inst = new T(),被重排为「1→3→2」:
    • 步骤 1:分配了内存;
    • 步骤 3:inst 指针已经指向这块内存(此时inst != NULL);
    • 步骤 2:还没执行(对象还没初始化,是 “半成品”)。
  2. 线程 B 此时走到 DCL 的「第一重检查」:if (inst == NULL),发现inst != NULL,直接返回这个指针;
  3. 线程 B 拿到inst后,试图调用对象的方法 / 访问成员变量 —— 但对象还没完成构造,结果就是程序崩溃、数据错乱、逻辑异常(比如访问未初始化的成员变量)。

volatile关键字的核心作用,就是给编译器 / CPU 下 “禁令”,针对被修饰的变量(比如volatile static T* inst):

  1. 禁止指令重排:编译器 / CPU 不能对涉及volatile变量的指令做重排 —— 也就是说,inst = new T()的三步必须严格按「1→2→3」执行,inst只有在对象完全构造后才会非 NULL;
  2. 禁止缓存优化:保证每次读写inst都是直接操作内存,而不是缓存到 CP

死锁

死锁是指多个执行流(进程 / 线程)在执行过程中,因争夺资源而造成的一种互相等待的现象。 如果没有外力干预,它们都将无法推进下去,程序就像 “卡死” 了一样。

形象比喻:两个人过独木桥,A 在桥头占着位置等 B 让路,B 在对面占着位置等 A 让路,结果谁也过不去。

死锁发生的四个必要条件

这四个条件缺一不可,只要破坏其中任意一个,死锁就不会发生。

  • 互斥条件资源是独占的,同一时刻只能被一个线程使用(如互斥锁)。这是锁的特性,通常无法破坏。

  • 请求与保持条件吃着碗里的,看着锅里的。线程已经持有了锁 A,在不释放 A 的情况下,去申请锁 B。

  • 不剥夺条件线程持有的资源,在未用完之前,不能被其他线程强行抢走。只能由它自己主动释放。

  • 循环等待条件A 等 B,B 等 C,...,Z 等 A。形成了一个闭环。

场景一:死锁现场

void deadlock_routine_A() { std::lock_guard<std::mutex> lock1(mtx1); std::cout << "[线程A] 获取了 mtx1,正在处理..." << std::endl; // 模拟处理耗时,确保线程B有机会获取 mtx2,形成死锁条件 std::this_thread::sleep_for(std::chrono::milliseconds(100)); std::cout << "[线程A] 尝试获取 mtx2..." << std::endl; std::lock_guard<std::mutex> lock2(mtx2); // 在这里阻塞,等待 mtx2 std::cout << "[线程A] 成功获取 mtx2,执行完毕。" << std::endl; } void deadlock_routine_B() { std::lock_guard<std::mutex> lock2(mtx2); std::cout << "[线程B] 获取了 mtx2,正在处理..." << std::endl; std::this_thread::sleep_for(std::chrono::milliseconds(100)); std::cout << "[线程B] 尝试获取 mtx1..." << std::endl; std::lock_guard<std::mutex> lock1(mtx1); // 在这里阻塞,等待 mtx1 std::cout << "[线程B] 成功获取 mtx1,执行完毕。" << std::endl; }

结果

[线程A] 获取了 mtx1... [线程B] 获取了 mtx2... [线程B] 尝试获取 mtx1... (等待) [线程A] 尝试获取 mtx2... (等待) (程序永久卡死)

这就构成了典型的环路等待:A -> mtx2 -> B -> mtx1 -> A。

破解法一

统一加锁顺序

void safe_routine_ordered1() { std::lock_guard<std::mutex> lock1(mtx1); std::cout << "[SafeThread1] 获取了 mtx1" << std::endl; std::this_thread::sleep_for(std::chrono::milliseconds(10)); std::lock_guard<std::mutex> lock2(mtx2); std::cout << "[SafeThread1] 获取了 mtx2" << std::endl; } void safe_routine_ordered2() { std::lock_guard<std::mutex> lock1(mtx1); std::cout << "[SafeThread2] 获取了 mtx1" << std::endl; std::this_thread::sleep_for(std::chrono::milliseconds(10)); std::lock_guard<std::mutex> lock2(mtx2); std::cout << "[SafeThread2] 获取了 mtx2" << std::endl; }

破解法二

使用 std::lock (C++标准库算法)

void safe_routine_std_lock() { // defer_lock 表示初始化时不立即加锁 std::unique_lock<std::mutex> lock1(mtx1, std::defer_lock); std::unique_lock<std::mutex> lock2(mtx2, std::defer_lock); std::cout << "[StdLockThread] 尝试同时获取 mtx1 和 mtx2..." << std::endl; // 原子性地锁定两个锁(避免死锁的核心) std::lock(lock1, lock2); std::cout << "[StdLockThread] 成功获取双锁!" << std::endl; // 退出作用域时自动解锁 }

原理:std::lock 内部使用了一种死锁避免算法(通常是 Try-and-Backoff 机制):

  1. 尝试锁住 lock1。
  2. 尝试锁住 lock2。
  3. 如果锁住 lock2 失败(被别人占了),它会主动释放 lock1(破坏请求与保持条件)。
  4. 等待一小会儿,然后重试,直到同时拿到两把锁。

破解法三

使用超时锁 (破坏不剥夺)使用 try_lock_for。"我尝试等 1 秒,如果拿不到锁 B,我就把自己手里的锁 A 释放掉,过会再来。"

在实际开发中,策略 1 (固定顺序) 和 策略 2 (std::lock) 是最有效的手段。

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

串口字符型LCD数据帧结构图解说明:通俗解释每一字段

串口字符型LCD通信协议图解&#xff1a;从“乱码”到精准控制的底层逻辑你有没有遇到过这种情况&#xff1f;MCU代码写得信心满满&#xff0c;UART线也接得严丝合缝&#xff0c;结果LCD屏上却只显示一堆乱码、空白或压根没反应。重启十次&#xff0c;换三次电源&#xff0c;甚至…

作者头像 李华
网站建设 2026/4/20 13:50:28

如何实现TensorRT推理服务的分级告警机制?

如何实现TensorRT推理服务的分级告警机制&#xff1f; 在当前AI模型大规模部署的背景下&#xff0c;一个看似“跑得通”的推理服务和真正“稳得住”的生产级系统之间&#xff0c;往往差了一套完善的可观测性体系。尤其是在自动驾驶、实时推荐、工业质检等对延迟与稳定性要求极高…

作者头像 李华
网站建设 2026/4/15 10:20:27

如何实现TensorRT推理服务的请求脱敏处理?

如何实现TensorRT推理服务的请求脱敏处理&#xff1f; 在金融、医疗等高敏感领域&#xff0c;AI模型正越来越多地参与核心业务决策。一个典型的场景是&#xff1a;某银行客服系统使用基于Transformer的自然语言理解模型分析用户对话&#xff0c;自动识别贷款意向或投诉情绪。然…

作者头像 李华
网站建设 2026/4/20 7:59:39

基于SpringBoot的大学生校外实习管理系统设计与实现毕业设计

博主介绍&#xff1a;✌ 专注于Java,python,✌关注✌私信我✌具体的问题&#xff0c;我会尽力帮助你。一、研究目的本研究旨在设计并实现一款基于SpringBoot的大学生校外实习管理系统&#xff0c;以解决当前大学生校外实习管理中存在的诸多问题。具体研究目的如下&#xff1a;提…

作者头像 李华
网站建设 2026/4/17 22:53:50

使用TensorRT优化OCR模型推理性能的实践

使用TensorRT优化OCR模型推理性能的实践 在智能文档处理、工业质检和金融票据识别等场景中&#xff0c;光学字符识别&#xff08;OCR&#xff09;正扮演着越来越关键的角色。然而&#xff0c;当我们将训练好的OCR模型投入生产环境时&#xff0c;往往面临一个尴尬的局面&#x…

作者头像 李华
网站建设 2026/4/12 14:59:53

ViGEmBus虚拟游戏手柄驱动完全配置手册

ViGEmBus虚拟游戏手柄驱动完全配置手册 【免费下载链接】ViGEmBus 项目地址: https://gitcode.com/gh_mirrors/vig/ViGEmBus 想要在Windows系统上体验多设备并行的专业级游戏控制吗&#xff1f;ViGEmBus虚拟游戏手柄驱动技术为你打开全新的大门&#xff01;这款强大的驱…

作者头像 李华