单例模式与锁(死锁)

news2025/6/9 11:21:50

目录

线程安全的单例模式

什么是单例模式

单例模式的特点  

饿汉实现方式和懒汉实现方式

饿汉⽅式实现单例模式

懒汉⽅式实现单例模式

懒汉⽅式实现单例模式(线程安全版本)

单例式线程池

ThreadPool.hpp

threadpool.cc

运行结果

线程安全和重⼊问题

常⻅锁概念

死锁

死锁四个必要条件

避免死锁

破坏死锁的四个必要条件 

避免死锁算法

STL中的容器是否是线程安全的?

智能指针是否是线程安全的?

其他常⻅的各种锁


线程安全的单例模式

什么是单例模式

单例模式的特点  

某些类, 只应该具有⼀个对象(实例), 就称之为单例.

例如⼀个男⼈只能有⼀个媳妇. 在很多服务器开发场景中, 经常需要让服务器加载很多的数据(上百G) 到内存中. 此时往往要⽤⼀个单例的类来管理这些数据.

单例模式 分为饿汉模式懒汉模式

饿汉实现方式和懒汉实现方式

[洗碗的例⼦]

吃完饭, ⽴刻洗碗, 这种就是饿汉⽅式. 因为下⼀顿吃的时候可以⽴刻拿着碗就能吃饭.

吃完饭, 先把碗放下, 然后下⼀顿饭⽤到这个碗了再洗碗, 就是懒汉⽅式.

饿汉模式: 直接准备好

懒汉模式: 用时才准备,延时加载        例如: 写时拷贝 ,new malloc 中真实物理空间的加载

懒汉方式最核心的思想是 "延时加载". 从而能够优化服务器的启动速度.

饿汉⽅式实现单例模式

template <typename T>
class Singleton 
{
     static T data;
public:
     static T* GetInstance() 
     {
         return &data;
     }
};

只要通过Singleton 这个包装类来使⽤ T 对象,则⼀个进程中只有⼀个T对象的实例.

懒汉⽅式实现单例模式

template <typename T>
class Singleton 
{
     static T* inst;
public:
 static T* GetInstance() 
 {
     if (inst == NULL) {
     inst = new T();
 }
     return inst;
 }
};

存在⼀个严重的问题, 线程不安全.

第⼀次调⽤ GetInstance 的时候, 如果两个线程同时调⽤,可能会创建出两份 T 对象的实例.但是后续再次调⽤,就没有问题了.

懒汉⽅式实现单例模式(线程安全版本)

// 懒汉模式, 线程安全 
template <typename T>
class Singleton 
{
     volatile static T* inst; // 需要设置 volatile 关键字, 否则可能被编译器优化. 
     static std::mutex lock;
public:
     static T* GetInstance() 
    {
         if (inst == NULL) 
        {    
             // 双重判定空指针, 降低锁冲突的概率, 提⾼性能. 
             lock.lock(); // 使⽤互斥锁, 保证多线程情况下也只调⽤⼀次 new. 
             if (inst == NULL) 
            {
                 inst = new T();
            }
             lock.unlock();
         }
         return inst;
    }
};

注意事项:

1. 加锁解锁的位置

2. 双重 if 判定,避免不必要的锁竞争

3. volatile关键字防⽌过度优化

单例式线程池

ThreadPool.hpp

#pragma once
#include <queue>
#include <iostream>
#include <string>
#include <vector> //用vector管理线程
#include <memory>
#include "log.hpp"
#include "Thread.hpp"
#include "Mutex.hpp"
#include "Cond.hpp"

namespace ThreadPoolModule
{
    using namespace LockModule;
    using namespace ThreadModule;
    using namespace CondModule;
    using namespace LogModule;

    const static int defaultnum = 5;
    using thread_t = std::shared_ptr<Thread>;

    void Defaulttest()
    {
        while (true)
        {
            LOG(LogLevel::INFO) << "test";
            sleep(1);
        }
    }

    template <typename T>
    class ThreadPool
    {
    private:
        bool IsEmpty()
        {
            return _taskq.empty();
        }

        // 执行任务
        void HandlerTask(std::string name)
        {
            LOG(LogLevel::INFO)<<"线程"<< name <<"进入handlerTask";
            // 线程醒来就一直执行
            while (true)
            {
                T t;
                {
                    LockGuard lockguard(_mutex);
                    while (IsEmpty() && _isrunning)
                    {
                        _wait_num++;
                        _cond.Wait(_mutex);
                        _wait_num--;
                    }
                    // 任务队列为空 && 线程池退出了才退出    ,重要重要 
                    if(IsEmpty() && !_isrunning)
                        break;


                    // 1.拿任务
                     t = _taskq.front();
                    _taskq.pop();
                }

                //在临界区外 ,处理任务 ,效率更高
                // 2.处理任务   规定传入的所有的任务, 必须提供()方法
                t(name);
            }
            LOG(LogLevel::INFO) << "线程: " << name << " 退出";
        }
        ThreadPool(const ThreadPool<T> &) = delete;//拷贝构造禁掉
        ThreadPool<T>& operator=(const ThreadPool<T> &) = delete;//赋值重载禁掉
        ThreadPool(int num = defaultnum) //构造函数设为私有
            : _num(num)
            ,_wait_num(0)
            ,_isrunning(false)
        {
            // 创建num个线程
            for (int i = 0; i < num; i++)
            {
                //bind  此时所有创建出来的线程,转而去执行HandlerTask
                _threads.push_back(std::make_shared<Thread>(std::bind(&ThreadPool::HandlerTask, this,std::placeholders::_1 )));
                LOG(LogLevel::INFO) << "构建线程" << _threads.back()->Name() << "成功";
            }
        }

    public:

        static ThreadPool<T> * getInstance() //单例模式
        {
	        if(_instance == NULL)
            {
	            LockGuard lockguard(_mutex_singleton);
                if(_instance == NULL)
                {
                    LOG(LogLevel::INFO) << "单例首次被执行,需要加载对象...";
                    _instance = new ThreadPool<T>();
                }
	        }

            return _instance;
        }
        
        ~ThreadPool()
        {
        }

        void Start()
        {
            if(_isrunning) return;
            _isrunning = true; // bug fix??

            for (auto &thread_ptr : _threads)
            {
                thread_ptr->Start();
                LOG(LogLevel::INFO) << "启动线程" << thread_ptr->Name() << "成功";
            }
        }

        void Wait()
        {
            for (auto &thread_ptr : _threads)
            {
                thread_ptr->Join();
                LOG(LogLevel::INFO) << "回收线程" << thread_ptr->Name() << "成功";

            }
        }

        //任务入队列
        void Enqueue(T &&in)//这个会被多线程调用 ,先加锁
        {
            //只要队列扛得住 ,就一直加
            LockGuard lockguard(_mutex);
            if(!_isrunning) return;
            _taskq.push(std::move(in));
            if(_wait_num > 0 ) 
                _cond.Notify();

        }

        //退出线程池
        void Stop()
        {
            LockGuard lockguard(_mutex);
            if(_isrunning)
            {
                // 3. 不能在入任务了
                _isrunning = false; // 不工作
                // 1. 让线程自己退出(要唤醒) && // 2. 历史的任务被处理完了
                if(_wait_num>0)
                    _cond.NotifyAll();
            }
        }

    private:
        int _num;                       // 线程个数
        std::queue<T> _taskq;           // 任务队列  是临界资源
        std::vector<thread_t> _threads; // 管理线程 ,其中是线程的指针

        Mutex _mutex;
        Cond _cond;
        int _wait_num;

        bool _isrunning ;               //线程池的运行状态

        static ThreadPool<T>* _instance; //单例模式的静态指针
        static Mutex _mutex_singleton;//只用来保护单例
    };

    //静态方法初始化应放在类外
    template<typename T>
    ThreadPool<T> *ThreadPool<T>::_instance = NULL;
    template<typename T>
    Mutex ThreadPool<T>::_mutex_singleton; //只用来保护单例
}

threadpool.cc

#include"ThreadPool.hpp"
#include<memory>
#include"Task.hpp"
using namespace ThreadPoolModule;


int main()
{
    ENABLE_CONSOLE_LOG();
    ThreadPool<task_t>::getInstance()->Start();
    char c;
    int cnt = 5;
    while (cnt)
    {
        // std::cin >> c;
        ThreadPool<task_t>::getInstance()->Enqueue(Push);
        cnt--;
        sleep(1);
    }

    ThreadPool<task_t>::getInstance()->Stop();
    ThreadPool<task_t>::getInstance()->Wait();
}

运行结果

线程安全和重⼊问题

线程安全描述的是线程

重入描述的是函数

线程安全:

就是多个线程在访问共享资源时,能够正确地执⾏,不会相互⼲扰或破坏彼此的执⾏结果。⼀般⽽⾔,多个线程并发同⼀段只有局部变量的代码时,不会出现不同的结果。但是对全局变量或者静态变量进⾏操作,并且没有锁保护的情况下,容易出现该问题。

重⼊:

同⼀个函数被不同的执⾏流调⽤,当前⼀个流程还没有执⾏完,就有其他的执⾏流再次进⼊, 我们称之为重⼊。⼀个函数在重⼊的情况下,运⾏结果不会出现任何不同或者任何问题,则该函数被 称为可重⼊函数,否则,是不可重⼊函数。

重⼊其实可以分为两种情况

  • 多线程重⼊函数
  • 信号导致⼀个执⾏流重复进⼊函数

常⻅锁概念

死锁

  • 死锁是指在⼀组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所占用不会释放的资源⽽处于的⼀种永久等待状态。
  • 为了方便表述,假设现在线程A,线程B必须同时持有锁1和锁2,才能进⾏后续资源的访问

申请⼀把锁是原⼦的,但是申请两把锁就不⼀定了

造成的结果是

死锁四个必要条件

  • 互斥条件:⼀个资源每次只能被⼀个执⾏流使用
  • 请求与保持条件:⼀个执⾏流因请求资源⽽阻塞时,对已获得的资源保持不放
  • 不剥夺条件:⼀个执⾏流已获得的资源,在末使⽤完之前,不能强⾏剥夺
  • 循环等待条件:若⼲执⾏流之间形成⼀种头尾相接的循环等待资源的关系

避免死锁

破坏死锁的四个必要条件 

破坏循环等待条件问题:资源⼀次性分配,使⽤超时机制、加锁顺序⼀致

// 下⾯的C++不写了,理解就可以 
#include <iostream>
#include <mutex>
#include <thread>
#include <vector>
#include <unistd.h>
// 定义两个共享资源(整数变量)和两个互斥锁 
int shared_resource1 = 0;
int shared_resource2 = 0;
std::mutex mtx1, mtx2;


// ⼀个函数,同时访问两个共享资源 
void access_shared_resources()
{
     // std::unique_lock<std::mutex> lock1(mtx1, std::defer_lock);
     // std::unique_lock<std::mutex> lock2(mtx2, std::defer_lock);
     // // 使⽤ std::lock 同时锁定两个互斥锁 
     // std::lock(lock1, lock2);
     // 现在两个互斥锁都已锁定,可以安全地访问共享资源 
     int cnt = 10000;
     while (cnt)
     {
         ++shared_resource1;
         ++shared_resource2;
         cnt--;
     }
     // 当离开 access_shared_resources 的作⽤域时,lock1 和 lock2 的析构函数会被⾃动调⽤ 
     // 这会导致它们各⾃的互斥量被⾃动解锁 
}

    // 模拟多线程同时访问共享资源的场景 
void simulate_concurrent_access()
{
     std::vector<std::thread> threads;
    // 创建多个线程来模拟并发访问 
     for (int i = 0; i < 10; ++i)
     {
         threads.emplace_back(access_shared_resources);
     }
     // 等待所有线程完成 
     for (auto &thread : threads)
     {
         thread.join();
     }
     // 输出共享资源的最终状态 
     std::cout << "Shared Resource 1: " << shared_resource1 << std::endl;
     std::cout << "Shared Resource 2: " << shared_resource2 << std::endl;
}

int main()
{
     simulate_concurrent_access();
     return 0;
}
$ ./a.out // 不⼀次申请 
Shared Resource 1: 94416
Shared Resource 2: 94536


$ ./a.out // ⼀次申请 
Shared Resource 1: 100000
Shared Resource 2: 100000

避免死锁算法

  • 死锁检测算法
  • 银⾏家算法

STL中的容器是否是线程安全的?

不安全.

原因是, STL 的设计初衷是将性能挖掘到极致, ⽽⼀旦涉及到加锁保证线程安全, 会对性能造成巨⼤的影响.  ⽽且对于不同的容器, 加锁⽅式的不同, 性能可能也不同(例如hash表的锁表和锁桶). 因此 STL 默认不是线程安全.

如果需要在多线程环境下使⽤, 往往需要调⽤者⾃⾏保证线程安全.

智能指针是否是线程安全的?

智能指针是安全的,指针指向的对象不一定.

对于 unique_ptr, 由于只是在当前代码块范围内⽣效, 因此不涉及线程安全问题.

对于 shared_ptr, 多个对象需要共⽤⼀个引⽤计数变量, 所以会存在线程安全问题. 但是标准库实现的时 候考虑到了这个问题, 基于原⼦操作(CAS)的⽅式保证 shared_ptr 能够⾼效, 原⼦的操作引⽤计数.

其他常⻅的各种锁

  • 悲观锁:在每次取数据时,总是担⼼数据会被其他线程修改,所以会在取数据前先加锁(读锁, 写锁,⾏锁等),当其他线程想要访问数据时,被阻塞挂起。
  • 乐观锁:每次取数据时候,总是乐观的认为数据不会被其他线程修改,因此不上锁。但是在更新 数据前,会判断其他数据在更新前有没有对数据进⾏修改。主要采⽤两种⽅式:版本号机制和 CAS操作。
  • CAS操作:当需要更新数据时,判断当前内存值和之前取得的值是否相等。如果相等则⽤新值更 新。若不等则失败,失败则重试,⼀般是⼀个⾃旋的过程,即不断重试。
  • ⾃旋锁,读写锁.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2405293.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

理解世界如淦泽,穿透黑幕需老谋

理解世界如淦泽&#xff0c;穿透黑幕需老谋 卡西莫多 2025年06月07日 安徽 极少主动跟别人提及恩师的名字&#xff0c;生怕自己比孙猴子不成器但又比它更能惹事的德行&#xff0c;使得老师跟着被拖累而脸上无光。不过老师没有象菩提祖师训诫孙猴子那样不能说出师傅的名字&a…

第三讲 Linux进程概念

1. 冯诺依曼体系结构 我们买了笔记本电脑, 里面是有很多硬件组成的, 比如硬盘, 显示器, 内存, 主板... 这些硬件不是随便放在一起就行的, 而是按照一定的结构进行组装起来的, 而具体的组装结构, 一般就是冯诺依曼体系结构 1.1. 计算机的一般工作逻辑 我们都知道, 计算机的逻…

stm32-c8t6实现语音识别(LD3320)

目录 LD3320介绍&#xff1a; 功能引脚 主要特色功能 通信协议 端口信息 开发流程 stm32c8t6代码 LD3320驱动代码&#xff1a; LD3320介绍&#xff1a; 内置单声道mono 16-bit A/D 模数转换内置双声道stereo 16-bit D/A 数模转换内置 20mW 双声道耳机放大器输出内置 5…

爬虫学习记录day1

什么是逆向&#xff1f; 数据加密 参数加密 表单加密扣js改写Python举例子 4.1 元素&#xff1a;被渲染的数据资源 动态数据 静态数据 如果数据是加密的情况则无法直接得到数据 4.2 控制台&#xff1a;输出界面 4.3 源代码页面 4.4 网络&#xff1a;抓包功能&#xff0c;获取浏…

agent基础概念

agent是什么 我个人认为agent并没有一个所谓完美的定义,它是一个比较活的概念,就像是你眼中的一个机器人你希望它做什么事,和我眼中的机器人它解决事情的流程,其实是可以完全不同的,没有必要非得搞一个统一的概念或流程来概况它。但我们依然可以概况几个通用的词来描述它…

让音乐“看得见”:使用 HTML + JavaScript 实现酷炫的音频可视化播放器

在这个数字时代,音乐不仅是听觉的享受,更可以成为视觉的盛宴!本文用 HTML + JavaScript 实现了一个音频可视化播放器,它不仅能播放本地音乐、控制进度和音量,还能通过 Canvas 绘制炫酷的音频频谱图,让你“听见色彩,看见旋律”。 效果演示 核心功能 本项目主要包含以下…

CAD实体对象智能识别

CAD实体对象智能识别 概述 实体对象智能识别能够在CAD图纸中智能识别和匹配相似的实体对象。该系统采用模式匹配算法&#xff0c;支持几何变换&#xff08;缩放、旋转&#xff09;&#xff0c;并提供了丰富的配置选项和可视化界面。 系统提供两种主要的识别方式&#xff1a;…

LabVIEW音频测试分析

LabVIEW通过读取指定WAV 文件&#xff0c;实现对音频信号的播放、多维度测量分析功能&#xff0c;为音频设备研发、声学研究及质量检测提供专业工具支持。 主要功能 文件读取与播放&#xff1a;支持持续读取示例数据文件夹内的 WAV 文件&#xff0c;可实时播放音频以监听被测信…

RoseMirrorHA 双机热备全解析

在数字化时代&#xff0c;企业核心业务系统一旦瘫痪&#xff0c;每分钟可能造成数万甚至数十万的损失。想象一下&#xff0c;如果银行的交易系统突然中断&#xff0c;或者医院的挂号系统无法访问&#xff0c;会引发怎样的连锁反应&#xff1f;为了守护这些关键业务&#xff0c;…

day 18进行聚类,进而推断出每个簇的实际含义

浙大疏锦行 对聚类的结果根据具体的特征进行解释&#xff0c;进而推断出每个簇的实际含义 两种思路&#xff1a; 你最开始聚类的时候&#xff0c;就选择了你想最后用来确定簇含义的特征&#xff0c; 最开始用全部特征来聚类&#xff0c;把其余特征作为 x&#xff0c;聚类得到…

LLMs 系列科普文(6)

截止到目前&#xff0c;我们从模型预训练阶段的数据准备讲起&#xff0c;谈到了 Tokenizer、模型的结构、模型的训练&#xff0c;基础模型、预训练阶段、后训练阶段等&#xff0c;这里存在大量的术语或名词&#xff0c;也有一些奇奇怪怪或者说是看起来乱七八糟的内容。这期间跳…

serv00 ssh登录保活脚本-邮件通知版

适用于自己有服务器情况&#xff0c;ssh定时登录到serv00&#xff0c;并在登录成功后发送邮件通知 msmtp 和 mutt安装 需要安装msmtp 和 mutt这两个邮件客户端并配置&#xff0c;参考如下文章前几步是讲配置这俩客户端的&#xff0c;很简单&#xff0c;不再赘述 用Shell脚本实…

意识上传伦理前夜:我们是否在创造数字奴隶?

当韩国财阀将“数字永生”标价1亿美元准入权时&#xff0c;联合国预警的“神经种姓制度”正从科幻步入现实。某脑机接口公司用户协议中“上传意识衍生算法归公司所有”的隐藏条款&#xff0c;恰似德里达预言的当代印证&#xff1a;“当意识沦为可交易数据流&#xff0c;主体性便…

【AIGC】RAGAS评估原理及实践

【AIGC】RAGAS评估原理及实践 &#xff08;1&#xff09;准备评估数据集&#xff08;2&#xff09;开始评估2.1 加载数据集2.2 评估忠实性2.3 评估答案相关性2.4 上下文精度2.5 上下文召回率2.6 计算上下文实体召回率 RAGas&#xff08;RAG Assessment)RAG 评估的缩写&#xff…

ESP12E/F 参数对比

模式GPIO0GPIO2GPIO15描述正常启动高高低从闪存运行固件闪光模式低高低启用固件刷写 PinNameFunction1RSTReset (Active Low)2ADC (A0)Analog Input (0–1V)3EN (CH_PD)Chip Enable (Pull High for Normal Operation)4GPIO16Wake from Deep Sleep, General Purpose I/O5GPIO14S…

第二十八章 字符串与数字

第二十八章 字符串与数字 计算机程序完全就是和数据打交道。很多编程问题需要使用字符串和数字这种更小的数据来解决。 参数扩展 第七章,已经接触过参数扩展,但未进行详细说明,大多数参数扩展并不用于命令行,而是出现在脚本文件中。 如果没有什么特殊原因,把参数扩展放…

[RDK X5] MJPG编解码开发实战:从官方API到OpenWanderary库的C++/Python实现

业余时间一直在基于RDK X5搞一些小研究&#xff0c;需要基于高分辨率图像检测目标。实际落地时&#xff0c;在图像采集上遇到了个大坑。首先&#xff0c;考虑到可行性&#xff0c;我挑选了一个性价比最高的百元内摄像头&#xff0c;已确定可以在X5上使用&#xff0c;接下来就开…

aardio 简单网页自动化

WebView自动化&#xff0c;以前每次重复做网页登录、搜索这些操作时都觉得好麻烦&#xff0c;现在终于能让程序替我干活了&#xff0c;赶紧记录下这个超实用的技能&#xff01; 一、初次接触WebView WebView自动化就像给程序装了个"网页浏览器"&#xff0c;第一步得…

打卡第39天:Dataset 和 Dataloader类

知识点回顾&#xff1a; 1.Dataset类的__getitem__和__len__方法&#xff08;本质是python的特殊方法&#xff09; 2.Dataloader类 3.minist手写数据集的了解 作业&#xff1a;了解下cifar数据集&#xff0c;尝试获取其中一张图片 import torch import torch.nn as nn import…

如何做好一份优秀的技术文档:专业指南与最佳实践

如何做好一份优秀的技术文档&#xff1a;专业指南与最佳实践 技术文档是产品开发、用户支持和团队协作的核心工具。高质量的技术文档能够提升开发效率、降低维护成本并改善用户体验。本文将从实践出发&#xff0c;详细讲解如何编写专业、清晰且实用的技术文档。 &#x1f31f;…