【Linux-多线程】-线程安全单例模式+可重入vs线程安全+死锁等

一、线程安全的单例模式

什么是单例模式

单例模式是一种"经典的,常用的,常考的"设计模式

什么是设计模式

IT行业这么火,涌入的人很多.俗话说林子大了啥鸟都有。大佬和菜鸡们两极分化的越来越严重,为了让菜鸡们不太拖大佬的后腿,于是大佬们针对一些经典的常见的场景,给定了一些对应的解决方案,这个就是 设计模式

单例模式的特点

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

例如一个男人只能有一个媳妇

在很多服务器开发场景中,经常需要让服务器加载很多的数据(上百G)到内存中,此时往往要用一个单例的类来管理这些数据

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

【洗碗的例子】

复制代码
吃完饭,立刻洗碗,这种就是饿汉方式.因为下一顿吃的时候可以立刻拿着碗就能
吃完饭,先把碗放下,然后下一顿饭用到这个碗了再洗碗,就是懒汉方式•

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

饿汉方式实现单例模式

懒汉方式实现单例模式

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

第一次调用 GetInstance的时候,如果两个线程同时调用,可能会创建出两份T对象的实例

但是后续再次调用,就没有问题了

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

cpp 复制代码
// 懒汉模式, 线程安全
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关键字防止过度优化

二、STL,智能指针和线程安全

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

不是. 原因是,STL的设计初衷是将性能挖掘到极致,而一旦涉及到加锁保证线程安全,会对性能造成巨大的影响. 而且对于不同的容器,加锁方式的不同,性能可能也不同(例如hash 表的锁表和锁桶).因此 STL 默认不是线程安全.如果需要在多线程环境下使用,往往需要调用者自行保证线程安全.

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

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

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

★ps:智能指针不等于智能指针对象,所以在应用中该加锁加锁

三、可重入 VS 线程安全

概念

  • 线程安全

多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题

  • 重入

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

★ps:可重入不可重入函数表示能不能被多个执行流同时进入

★ps:线程安全表示多线程并发执行一段代码会不会出错

常见的线程不安全的情况

❍ 不保护共享变量的函数

❍ 函数状态随着被调用,状态发生变化的函数

❍ 返回指向静态变量指针的函数

❍ 调用线程不安全函数的函数

常见的线程安全的情况

◉ 每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的

◉ 类或者接口对于线程来说都是原子操作

◉ 多个线程之间的切换不会导致该接口的执行结果存在二义性

常见不可重入的情况

✸ 调用了 malloc/free 函数,因为 malloc 函数是用全局链表来管理堆的

✸ 调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构

✸ 可重入函数体内使用了静态的数据结构

常见可重入的情况

❍ 不使用全局变量或者静态变量

❍ 不使用malloc 或者 new 开辟出的空间

❍ 不调用不可重入函数

❍ 不返回静态或全局数据,所有数据都有函数的调用者提供

❍ 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据

可重入与线程安全联系

✸ 函数是可重入的,那线程就是安全的

✸ 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题

✸ 如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的

可重入与线程安全的区别

◉ 可重入函数是线程安全函数的一种

◉ 线程安全不一定是可重入的,而可重入函数则一定是安全的

◉ 如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的

四、死锁

概念

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

死锁的四个必要条件

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

避免死锁

❍ 破坏死锁的四个必要条件

❍ 加锁顺序一致

❍ 避免锁未释放的场景

❍ 资源一次性分配

避免死锁的算法

死锁检测算法

银行家算法

相关推荐
明月看潮生1 天前
青少年编程与数学 01-011 系统软件简介 02 UNIX操作系统
服务器·青少年编程·操作系统·unix·系统软件
明月看潮生1 天前
青少年编程与数学 01-011 系统软件简介 05 macOS操作系统
macos·青少年编程·操作系统·系统软件·编程与数学
Lumos_2 天前
OpenEluer 安装 OpenLDAP
操作系统
GoGeekBaird2 天前
69天探索操作系统-第69天:高级进程调度:实时和基于优先级的任务管理技术
后端·操作系统
GoGeekBaird3 天前
69天探索操作系统-第68天:从用户到内核:实现动态系统调用处理以构建健壮的操作系统
后端·操作系统
免檒3 天前
第二章 进程管理
算法·操作系统
egoist20233 天前
【Linux仓库】冯诺依曼体系结构与操作系统【进程·壹】
linux·运维·服务器·开发语言·操作系统·冯诺依曼体系结构
望获linux4 天前
【Linux基础知识系列】第八篇-基本网络配置
linux·数据库·postgresql·操作系统·php·开源软件·rtos
GoGeekBaird4 天前
69天探索操作系统-第67天:从恐慌到解决:实施内核调试技术进行崩溃分析
后端·操作系统
shark-chili4 天前
Java并发编程哲学系列汇总
linux·运维·服务器·操作系统