🔥个人主页: Milestone-里程碑
❄️个人专栏: <<力扣hot100>> <<C++>><<Linux>>
🌟心向往之行必能至
目录
[1.2 实现方式](#1.2 实现方式)
[1.3 解决](#1.3 解决)
[3.1 4个必要条件](#3.1 4个必要条件)
[3.2 避免死锁](#3.2 避免死锁)
一.线程安全的单例模式
单例模式的特点
某些类, 只应该具有⼀个对象(实例), 就称之为单例.
例如⼀个男⼈只能有⼀个媳妇.
在很多服务器开发场景中, 经常需要让服务器加载很多的数据 (上百G) 到内存中. 此时往往要⽤⼀个单例的类来管理这些数据.
1.1实现方式:饿汉模式和懒汉模式
洗碗的例⼦
吃完饭 , ⽴刻洗碗 , 这种就是饿汉⽅式 . 因为下⼀顿吃的时候可以⽴刻拿着碗就能吃饭 .
吃完饭 , 先把碗放下 , 然后下⼀顿饭⽤到这个碗了再洗碗 , 就是懒汉⽅式
懒汉⽅式最核⼼的思想是 "延时加载". 从⽽能够优化服务器的启动速度.
我们前面的malloc new这些都采用懒汉模式的思想,只开辟了虚拟地址,而物理地址并未开辟,在被正式用之前,可被其他线程调用,再释放物理地址,提高内存的使用率
1.2 实现方式
bash
饿汉⽅式实现单例模式
template <typename T>
class Singleton {
static T data;
public:
static T* GetInstance() {
return &data;
}
};
bash
懒汉⽅式实现单例模式
template <typename T>
class Singleton {
static T* inst;
public:
static T* GetInstance() {
if (inst == NULL) {
inst = new T();
}
return inst;
}
};
但懒汉模式有一个问题,可能线程不安全
如果第一次调用GetInstance时,有多个线程进行调用呢,会创建出多份 T 对象的实例.
1.3 解决
bash
懒汉⽅式实现单例模式(线程安全版本)
// 懒汉模式, 线程安全
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关键字防⽌过度优化
二.线程安全和重入问题
线程安全:就是多个线程在访问共享资源时,能够正确地执⾏,不会相互⼲扰或破坏彼此的执⾏结
果。⼀般⽽⾔,多个线程并发同⼀段只有局部变量的代码时,不会出现不同的结果。但是对全局变量 或者静态变量进⾏操作,并且没有锁保护的情况下,容易出现该问题。
重⼊:同⼀个函数被不同的执⾏流调⽤,当前⼀个流程还没有执⾏完,就有其他的执⾏流再次进⼊, 我们称之为重⼊。⼀个函数在重⼊的情况下,运⾏结果不会出现任何不同或者任何问题,则该函数被称为可重⼊函数,否则,是不可重⼊函数。
可重⼊与线程安全联系
• 函数是可重⼊的,那就是线程安全的(其实知道这⼀句话就够了)
• 函数是不可重⼊的,那就不能由多个线程使⽤,有可能引发线程安全问题
• 如果⼀个函数中有全局变量,那么这个函数既不是线程安全也不是可重⼊的。
可重⼊与线程安全区别
可重⼊函数是线程安全函数的⼀种
线程安全不⼀定是可重⼊的,⽽可重⼊函数则⼀定是线程安全的。
如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重⼊函数若锁还
未释放则会产⽣死锁,因此是不可重⼊的。
•
如果不考虑 信号导致⼀个执⾏流重复进⼊函数 这种重⼊情况,线程安全和重⼊在安全⻆
度不做区分
•
但是线程安全侧重说明线程访问公共资源的安全情况,表现的是并发线程的特点
•
可重⼊描述的是⼀个函数是否能被重复进⼊,表⽰的是函数的特点
三.死锁
•
死锁是指在⼀组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所占⽤不会
释放的资源⽽处于的⼀种永久等待状态。
例子理解,
你和小明都各有5毛,去小卖部买棒棒糖,当要1块钱,你让小明把钱给你,小明让你把钱给他,这样你们就陷入了僵局
3.1 4个必要条件
互斥条件:⼀个资源每次只能被⼀个执⾏流使⽤
请求与保持条件:⼀个执⾏流因请求资源⽽阻塞时,对已获得的资源保持不放
不剥夺条件:⼀个执⾏流已获得的资源,在末使⽤完之前,不能强⾏剥夺
循环等待条件:若⼲执⾏流之间形成⼀种头尾相接的循环等待资源的关系(你等着小明给你,小明等着你给他)
3.2 避免死锁
即破坏上面的任一一个条件都可
破坏死锁的四个必要条件
破坏循环等待条件问题:资源⼀次性分配, 使⽤超时机制、加锁顺序⼀致
四.stl,智能指针和线程安全
stl的线程不是安全的
原因是, STL 的设计初衷是将性能挖掘到极致, ⽽⼀旦涉及到加锁保证线程安全, 会对性能造成巨⼤的影 响.⽽且对于不同的容器, 加锁⽅式的不同, 性能可能也不同(例如hash表的锁表和锁桶). 因此 STL 默认不是线程安全. 如果需要在多线程环境下使⽤, 往往需要调⽤者⾃⾏保证线程安全
对于 unique_ptr, 由于只是在当前代码块范围内⽣效, 因此不涉及线程安全问题.
对于 shared_ptr, 多个对象需要共⽤⼀个引⽤计数变量, 所以会存在线程安全问题. 但是标准库实现的时候考虑到了这个问题, 基于原⼦操作(CAS)的⽅式保证 shared_ptr 能够⾼效, 原⼦的操作引⽤计数