单例模式与线程安全

目录

线程安全和重⼊问题

死锁和活锁

死锁

死锁四个必要条件

活锁

STL,智能指针和线程安全

线程安全的单例模式

饿汉模式

懒汉模式

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

饿汉模式实现单例模式


我们来学习单例模式与线程安全

线程安全和重⼊问题

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

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

学到现在,其实我们已经能理解重⼊其实可以分为两种情况 :多线程重⼊函数 和信号导致⼀个执⾏流重复进⼊函数

所以上面这些都可以不用看的,直接输出结论:

全局变量属于临界资源。

死锁和活锁

死锁

死锁是指在⼀组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站⽤不会 释放的资源⽽处于的⼀种永久等待状态。

死锁是指两个或多个线程因为相互等待对方释放资源,导致它们永远无法继续执行的状态。

就是多个线程申请多个未释放的锁,然后他们各自却持有这些锁(未解锁),导致都需要等待对方释放,比如A线程持有锁a,B线程持有锁b,那B今天来申请锁A,A申请锁b,这样A,B线程都需要各自等待对方释放才能申请成功,跟那个shared_ptr的循环引用无法释放一样的道理。

死锁本质上就是多个线程交叉持有对方需要的资源,但又都不释放,导致所有线程都卡死在等待状态。

只有一个线程申请锁时也可能导致死锁的,自己重复申请自己上次没有释放的锁,怎么解决死锁问题,只能使用外部外部干预,要干预首先了解死锁四个必要条件。

死锁四个必要条件

互斥条件:⼀个资源每次只能被⼀个执⾏流使⽤,如果多个执行流可以访问同一个锁,怎么可能会死锁呢。破坏这个条件不太可能,违背锁的意愿。

请求与保持条件:⼀个执⾏流因请求资源⽽阻塞时,对已获得的资源保持不放。

不剥夺条件:⼀个执⾏流已获得的资源,在末使⽤完之前,不能强⾏剥夺,就是一个线程使用锁进入临界区访问具有原子性,还没访问完不能解锁。

循环等待条件:若⼲执⾏流之间形成⼀种头尾相接的循环等待资源的关系,导致死锁完全是在锁上等待导致的。打破这个简单呀,使用try_lock进行申请锁就可以了呀,锁不可用就返回false。

所以如何避免死锁就是破坏死锁的四个必要条件中的一个就可以了,破坏循环等待条件问题:资源⼀次性分配,使⽤超时机制、加锁顺序⼀致,也可以避免锁未释放的场景。

活锁

活锁是指多个线程不断地 相互让步,但因为策略问题,导致它们无法取得进展,活锁和死锁的区别就是线程不会一直等待一个没有解锁的锁,而是一直不断的申请,直至申请成功。相当于这些线程一直在申请结果没有成功,没有成功还一直在申请。

由于不停的申请锁会带来CPU的高占用,所以也是程序禁止的一种情况。

STL,智能指针和线程安全

STL中的容器不一定都是线程安全的,大部分函数都不支持重入所以都是线程不安全的,STL的设计初衷是将性能挖掘到极致,⽽⼀旦涉及到加锁保证线程安全,会对性能造成巨⼤的影 响。⽽且对于不同的容器,加锁⽅式的不同,性能可能也不同(例如hash表的锁表和锁桶)。因此STL默认不是线程安全,如果需要在多线程环境下使⽤,往往需要调⽤者⾃⾏保证线程安全。

智能指针是线程安全的,所以我们才愿意将一个线程交给他管理,对于unique_ptr,由于只是在当前代码块范围内⽣效,因此不涉及线程安全问题。

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

但是智能指针指向的对象不一定是线程安全的,只是智能指针的操作的线程安全的。

线程安全的单例模式

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

单例模式主要分为饿汉模式和懒汉模式。要实现单例那这两种模式都是禁止拷贝的,而且初始化函数必须放为私有,不让外部直接通过构造函数访问。

饿汉模式

吃完饭, ⽴刻洗碗, 这种就是饿汉⽅式.,因为下⼀顿吃的时候可以⽴刻拿着碗就能吃饭。也就是饿汉模式里面肯定已经创建出了一个静态的对象,等你要调用的时候,通过一个static函数返回,保证返回函数,和创建的静态变量在类中只有一份,防止拷贝。

懒汉模式

吃完饭, 先把碗放下, 然后下⼀顿饭⽤到这个碗了再洗碗, 就是懒汉⽅式,就是需要时才创建对象的机制,属于延迟创建/加载,同样也只有一个对象,所以new出来的对象需要装在一个static指针里面,而这个指针是类里面已经创建好的。

反正需要时才调用getinstance创建,然后返回。存在⼀个严重的问题,线程不安全。 第⼀次调⽤GetInstance的时候,如果两个线程同时调⽤,可能会创建出两份T对象的实例, 但是后续再次调⽤,就没有问题了,此时inst就不是空了,不能创建对象了。

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

#pragma once
#include<iostream>
using namespace std;
#include<memory>
#include<unistd.h>
#include<vector>
#include<string>
#include<queue>
#include<functional> //
#include"pthread.hpp"
#include"mutex.hpp"
#include"cond.hpp"
#include"log.hpp"namespace threadpoolmodule
{using namespace lockmodule;using namespace threadmodule;using namespace condmodule;using namespace logmodule;using thread_t = shared_ptr<thread>;//用来做测试的线程方法void defaulttest(){while (true){LOG(loglevel::DEBUG) << "我是一个测试方法";sleep(1);}}const static int defaultnum = 5;template<class T>class threadpool{private:bool isempty(){return _taskq.empty();}void HandleTask(string name){LOG(loglevel::INFO) << "线程进入HandleTask的逻辑\n";while (true){T t;{lockguard lockguard(mut);// 1.拿任务while (isempty() && _isrunning){_wait_num++;_cond.wait(mut);_wait_num--;}if (isempty() && !_isrunning){break;}t = _taskq.front();_taskq.pop();}//2 处理任务t(name); //规定,未来所有的任务处理都是必须提供()直接调用的方法}LOG(loglevel::INFO) << "线程:" << name << "退出";}private:threadpool(): _num(defaultnum), _wait_num(0),_isrunning(false){for (int i = 0; i < _num; i++){//_threads.push_back(make_shared<thread>(bind(&threadpool::HandleTask, this, placeholders::_1)));LOG(loglevel::INFO) << "构键线程" << _threads.back()->getname() << "对象 。。。成功";}}public:threadpool<T>& operator=(const threadpool<T>&) = delete; //使用operator=的赋值拷贝不能用了threadpool(const threadpool<T>&) = delete;  //赋值拷贝不能用了static threadpool<T>* getinstance(){LOG(loglevel::INFO) << "单例首次被执行,需要加载对象...";if (instance == nullptr){instance = new threadpool<T>();}return instance;}void start(){if (_isrunning){return;}_isrunning = true;for (auto& thread_ptr : _threads){thread_ptr->start();LOG(loglevel::INFO) << "启动线程" << thread_ptr->getname() << "。。。成功";}}void equeue(T in){lockguard lockguard(mut);if (!_isrunning){return;}_taskq.push(move(in));if ( _wait_num){_cond.notify();}}void stop(){//让里面的线程都重新自己退出//退出之前需要将所有的任务都处理完,所以需要一次性唤醒所有在等待的线程if (_isrunning){_isrunning = false;if (_wait_num){_cond.notifyall();}}}void join(){for (auto& thread_ptr : _threads){thread_ptr->join();LOG(loglevel::INFO) << "停止线程" << thread_ptr->getname() << "。。。成功";}}~threadpool(){}private:vector<thread_t> _threads;  //线程池中的线程初始个数在数组里面int _num;   //线程池的容量queue<T> _taskq;  //线程池中的任务队列mutex mut;cond _cond;int _wait_num; //等待队列线程bool _isrunning;static threadpool<T>* instance;};template<class T>threadpool<T>* threadpool<T>::instance = nullptr; //在外面初始化, 私有的static成员可以在类外初始化
};

instance必须放私有,这样比较安全。

懒汉模型这种延迟生成,按需生成的技术在操作系统内核用的还是比较多的,像什么写时拷贝,malloc创建空间,物理地址的映射都是。

“不到万不得已,绝不提前分配”,这种按需分配的策略在操作系统内核中极为常见,既能提升性能,又能节省资源。

但是外面的getinstance方法还没有加锁保护,所以需要多增加一个锁进行保护。进入函数的时候立即加锁,加锁前需要先判断是否满足instance是空的,不然白加锁了,为什么使用双重判断呢,外层是为了防止白加锁,内层是为了防止加锁失败或者其他原因导致多个线程进来从而导致instance被赋值两次。

静态成员函数不能直接访问类的非静态成员变量,所以这个锁也应该是静态的才可以被访问到。

private:vector<thread_t> _threads;  //线程池中的线程初始个数在数组里面int _num;   //线程池的容量queue<T> _taskq;  //线程池中的任务队列mutex mut;static mutex _lock;cond _cond;int _wait_num; //等待队列线程bool _isrunning;static threadpool<T>* instance;};template<class T>threadpool<T>* threadpool<T>::instance = nullptr; //在外面初始化, 私有的static成员可以在类外初始化template<class T>mutex threadpool<T>::_lock;

饿汉模式实现单例模式

#pragma once
#include<iostream>
using namespace std;
#include<memory>
#include<unistd.h>
#include<vector>
#include<string>
#include<queue>
#include<functional> //
#include"pthread.hpp"
#include"mutex.hpp"
#include"cond.hpp"
#include"log.hpp"namespace threadpoolmodule
{using namespace lockmodule;using namespace threadmodule;using namespace condmodule;using namespace logmodule;using thread_t = shared_ptr<thread>;//用来做测试的线程方法void defaulttest(){while (true){LOG(loglevel::DEBUG) << "我是一个测试方法";sleep(1);}}const static int defaultnum = 5;template<class T>class threadpool{private:bool isempty(){return _taskq.empty();}void HandleTask(string name){LOG(loglevel::INFO) << "线程进入HandleTask的逻辑\n";while (true){T t;{lockguard lockguard(mut);// 1.拿任务while (isempty() && _isrunning){_wait_num++;_cond.wait(mut);_wait_num--;}if (isempty() && !_isrunning){break;}t = _taskq.front();_taskq.pop();}//2 处理任务t(name); //规定,未来所有的任务处理都是必须提供()直接调用的方法}LOG(loglevel::INFO) << "线程:" << name << "退出";}private:threadpool(): _num(defaultnum), _wait_num(0),_isrunning(false){for (int i = 0; i < _num; i++){//_threads.push_back(make_shared<thread>(bind(&threadpool::HandleTask, this, placeholders::_1)));LOG(loglevel::INFO) << "构键线程" << _threads.back()->getname() << "对象 。。。成功";}}public:threadpool<T>& operator=(const threadpool<T>&) = delete; //使用operator=的赋值拷贝不能用了threadpool(const threadpool<T>&) = delete;  //赋值拷贝不能用了static threadpool<T>* getinstance(){LOG(loglevel::INFO) << "单例首次被执行,需要加载对象...";return &instance;}void start(){if (_isrunning){return;}_isrunning = true;for (auto& thread_ptr : _threads){thread_ptr->start();LOG(loglevel::INFO) << "启动线程" << thread_ptr->getname() << "。。。成功";}}void equeue(T in){lockguard lockguard(mut);if (!_isrunning){return;}_taskq.push(move(in));if ( _wait_num){_cond.notify();}}void stop(){//让里面的线程都重新自己退出//退出之前需要将所有的任务都处理完,所以需要一次性唤醒所有在等待的线程if (_isrunning){_isrunning = false;if (_wait_num){_cond.notifyall();}}}void join(){for (auto& thread_ptr : _threads){thread_ptr->join();LOG(loglevel::INFO) << "停止线程" << thread_ptr->getname() << "。。。成功";}}~threadpool(){}private:vector<thread_t> _threads;  //线程池中的线程初始个数在数组里面int _num;   //线程池的容量queue<T> _taskq;  //线程池中的任务队列mutex mut;cond _cond;int _wait_num; //等待队列线程bool _isrunning;static threadpool<T> instance;};template<class T>threadpool<T> threadpool<T>::instance; //在外面初始化, 私有的static成员可以在类外初始化
};

这个实现起来差不多呀,而且完全没有线程安全问题,因为就一个对象。

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

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

相关文章

Python+AI提示词用贝叶斯样条回归拟合BSF方法分析樱花花期数据模型构建迹图、森林图可视化

原文链接&#xff1a;https://tecdat.cn/?p41308 在数据科学的领域中&#xff0c;我们常常会遇到需要处理复杂关系的数据。在众多的数据分析方法中&#xff0c;样条拟合是一种非常有效的处理数据非线性关系的手段。本专题合集围绕如何使用PyMC软件&#xff0c;对樱花花期数据进…

WPF学习路线

WPF学习路线 学习准备学习技术栈学习路线 1-5&#xff08;1-2周&#xff09;6-8&#xff08;3-5周&#xff09; 学习准备 个人认为前端技术一般几个关键字&#xff1a;元素资源 控制元素资源组合或者动态交互 数据交互呈现分析关键字得到的就是几个方向 布局 样式 组装资源控件…

31天Python入门——第20天:魔法方法详解

你好&#xff0c;我是安然无虞。 文章目录 魔法方法1. __new__和__del__2. __repr__和__len__3. __enter__和__exit__4. 可迭代对象和迭代器5. 中括号[]数据操作6. __getattr__、__setattr__ 和 __delattr__7. 可调用的8. 运算符 魔法方法 魔法方法: Python中的魔法方法是一类…

栈 —— 数据结构基础刷题路程

一、P1739 表达式括号匹配 - 洛谷 算法代码&#xff1a; #include<bits/stdc.h> using namespace std; const int N300008; struct mystack {int a[N];int t-1;//压栈void push(int data){a[t]data; } //取栈顶元素int top(){return a[t]; } //弹出栈顶元素void pop(){i…

瑞昱RTD2556QR显示器驱动芯片

一、概述 RTD2556QR芯片是由Realtek公司精心研发的一款高性能显示驱动芯片&#xff0c;专为满足现代显示设备对高分辨率、多功能接口及稳定性能的需求而设计。该芯片凭借其卓越的技术特性和广泛的应用领域&#xff0c;在显示驱动市场中占据重要地位。它集成了多种先进的功能模…

PyQt5和OpenCV车牌识别系统

有需要请加文章底部Q哦 可远程调试 PyQt5和OpenCV车牌识别系统 一 介绍 此车牌识别系统基于PyQt5和OpenCV开发&#xff0c;蓝牌&#xff0c;新能源(绿牌)&#xff0c;黄牌&#xff0c;白牌均可以准确识别&#xff0c;支持中文识别&#xff0c;可以导出识别结果(Excel格式)。此…

学有所记- 探索FastAPI在docker上的部署

目标&#xff1a; 学习怎样在docker中安装部署FastAPI&#xff0c;完成项目结构的搭建以及hello world的运行 背景&#xff1a; 公司内服务器资源有限&#xff0c;为了共享算力资源&#xff0c;同时又能隔离运行环境&#xff0c;因此采用了docker部署的方式&#xff0c;进行各…

HTTP keepalive 详解

一、简介 HTTP协议早期版本&#xff0c;比如1.0&#xff0c;默认是不使用持久连接的&#xff0c;也就是每个请求/响应之后都会关闭TCP连接。这样的话&#xff0c;每次请求都需要重新建立连接&#xff0c;增加了延迟和资源消耗。Keep-Alive的作用是保持连接&#xff0c;让多个请…

长短期记忆神经网络(LSTM)基础学习与实例:预测序列的未来

目录 1. 前言 2. LSTM的基本原理 2.1 LSTM基本结构 2.2 LSTM的计算过程 3. LSTM实例&#xff1a;预测序列的未来 3.1 数据准备 3.2 模型构建 3.3 模型训练 3.4 模型预测 3.5 完整程序预测序列的未来 4. 总结 1. 前言 在深度学习领域&#xff0c;循环神经网络&…

基于机器学习的三国时期诸葛亮北伐失败因素量化分析

一、研究背景与方法论 1.1 历史问题的数据化挑战 三国时期&#xff08;220-280年&#xff09;的战争史存在史料分散、数据缺失的特点。本研究通过构建包含军事、经济、地理、政治四大维度的结构化数据库&#xff0c;收录建安十二年&#xff08;207年&#xff09;至建兴十二年…

蓝桥杯省模拟赛 数位和

问题描述 只能被 1 和本身整除的数称为质数。 请问在 1 &#xff08;含&#xff09;到 1000000 &#xff08;含&#xff09;中&#xff0c;有多少个质数的各个数位上的数字之和为 23 。 提示&#xff1a;599 就是这样一个质数&#xff0c;各个数位上的数字之和为 59923 。 #…

Timer的底层实现原理?

Timer 是 Java 中用于定时任务调度的基础工具类,其底层实现基于 单线程+任务队列 的模型。以下是 Timer 的底层实现原理的详细分析: 一、核心组件 TimerThread 继承自 Thread,是 Timer 的工作线程,负责从队列中提取任务并执行。通过 while (true) 循环持续检查任务队列。Ta…

Java 枚举类 Key-Value 映射的几种实现方式及最佳实践

Java 枚举类 Key-Value 映射的几种实现方式及最佳实践 前言 在 Java 开发中&#xff0c;枚举(Enum)是一种特殊的类&#xff0c;它能够定义一组固定的常量。在实际应用中&#xff0c;我们经常需要为枚举常量添加额外的属性&#xff0c;并实现 key-value 的映射关系。本文将详细…

青少年编程与数学 02-015 大学数学知识点 01课题、概要

青少年编程与数学 02-015 大学数学知识点 01课题、概要 一、线性代数二、概率论与数理统计三、微积分四、优化理论五、离散数学六、数值分析七、信息论 《青少年编程与数学》课程要求&#xff0c;在高中毕业前&#xff0c;尽量完成大部分大学数学知识的学习。一般可以通过线上课…

智能打印预约系统:微信小程序+SSM框架实战项目

微信小程序打印室预约系统&#xff0c;采用SSM&#xff08;SpringSpringMVCMyBatis&#xff09;经典框架组合。 一、系统核心功能详解 1. 智能化管理后台 ​用户数据看板​打印店资源管理​预约动态监控​服务评价系统 2. 微信小程序端 ​智能定位服务​预约时段选择​文件…

DataX 3.0 实战案例

第五章 实战案例 5.1. 案例一 5.1.1. 案例介绍 MySQL数据库中有两张表&#xff1a;用户表(users)&#xff0c;订单表(orders)。其中用户表中存储的是所有的用户的信息&#xff0c;订单表中存储的是所有的订单的信息。表结构如下&#xff1a; 用户表 users: id&#xff1a;用…

设计模式学习(1)

面向对象设计原则 单一职责 每个类只有一个职责&#xff0c;并被完整的封装在类中&#xff0c;该原则用来控制类的粒度。 例如Mapper&#xff0c;controller都只负责一个业务。 开闭原则 应该对扩展开放&#xff0c;而对修改封闭&#xff0c;例如定义接口或是抽象类作为抽…

在 Rocky Linux 9.2 上编译安装 Redis 6.2.6

文章目录 在 Rocky Linux 9.2 上编译安装 Redis 6.2.6Redis 介绍官网Redis 的核心特性高性能支持多种数据结构多种持久化机制复制与高可用2.5 事务与 Lua 脚本消息队列功能 Redis 适用场景Redis 与其他数据库对比Redis 的优势与劣势Redis 优势Redis 劣势 部署过程系统环境信息环…

量子计算与经典计算的融合与未来

最近研学过程中发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击下方超链接跳转到网站人工智能及编程语言学习教程。读者们可以通过里面的文章详细了解一下人工智能及其编程等教程和学习方法。下面进入文章正…

数据结构(4)——带哨兵位循环双向链表

目录 前言 一、带哨兵的循环双向链表是什么 二、链表的实现 2.1规定结构体 2.2创建节点 2.3初始化 2.4打印 2.5检验是否为空 2.6销毁链表 2.7尾插 2.8尾删 2.9头插 2.10头删 2.11寻找特定节点 2.12任意位置插入&#xff08;pos前&#xff09; 2.13删除任意节点 …