【Linux-多线程】-线程安全单例模式+可重入vs线程安全+死锁等
一、线程安全的单例模式
什么是单例模式
单例模式是一种“经典的,常用的,常考的”设计模式
什么是设计模式
IT行业这么火,涌入的人很多.俗话说林子大了啥鸟都有。大佬和菜鸡们两极分化的越来越严重,为了让菜鸡们不太拖大佬的后腿,于是大佬们针对一些经典的常见的场景,给定了一些对应的解决方案,这个就是 设计模式
单例模式的特点
某些类,只应该具有一个对象(实例),就称之为单例
例如一个男人只能有一个媳妇
在很多服务器开发场景中,经常需要让服务器加载很多的数据(上百G)到内存中,此时往往要用一个单例的类来管理这些数据
饿汉实现方式和懒汉实现方式
【洗碗的例子】
吃完饭,立刻洗碗,这种就是饿汉方式.因为下一顿吃的时候可以立刻拿着碗就能 吃完饭,先把碗放下,然后下一顿饭用到这个碗了再洗碗,就是懒汉方式•
懒汉方式最核心的思想是“延时加载”,从而能够优化服务器的启动速度
饿汉方式实现单例模式
懒汉方式实现单例模式
存在一个严重的问题,线程不安全
第一次调用 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;
}
};
注意事项:
-
加锁解锁的位置
-
双重 if 判定, 避免不必要的锁竞争
-
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 开辟出的空间
❍ 不调用不可重入函数
❍ 不返回静态或全局数据,所有数据都有函数的调用者提供
❍ 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据
可重入与线程安全联系
✸ 函数是可重入的,那线程就是安全的
✸ 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
✸ 如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的
可重入与线程安全的区别
◉ 可重入函数是线程安全函数的一种
◉ 线程安全不一定是可重入的,而可重入函数则一定是安全的
◉ 如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的
四、死锁
概念
死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用 不会释放的资源而处于的一种永久等待状态。
死锁的四个必要条件
✸ 互斥条件:一个资源每次只能被一个执行流使用 ✸ 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放 ✸ 不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺 ✸ 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系
避免死锁
❍ 破坏死锁的四个必要条件
❍ 加锁顺序一致
❍ 避免锁未释放的场景
❍ 资源一次性分配
避免死锁的算法
死锁检测算法
银行家算法