37 线程安全单例模式深度解析

🔥个人主页: Milestone-里程碑

❄️个人专栏: <<力扣hot100>> <<C++>><<Linux>>

🌟心向往之行必能至

目录

一.线程安全的单例模式

1.1实现方式:饿汉模式和懒汉模式

[1.2 实现方式](#1.2 实现方式)

[1.3 解决](#1.3 解决)

二.线程安全和重入问题

三.死锁

[3.1 4个必要条件](#3.1 4个必要条件)

[3.2 避免死锁](#3.2 避免死锁)

四.stl,智能指针和线程安全


一.线程安全的单例模式

单例模式的特点

某些类, 只应该具有⼀个对象(实例), 就称之为单例.
例如⼀个男⼈只能有⼀个媳妇.
在很多服务器开发场景中, 经常需要让服务器加载很多的数据 (上百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;
}
};

注意事项:

  1. 加锁解锁的位置
  2. 双重 if 判定, 避免不必要的锁竞争
  3. volatile关键字防⽌过度优化

二.线程安全和重入问题

线程安全:就是多个线程在访问共享资源时,能够正确地执⾏,不会相互⼲扰或破坏彼此的执⾏结
果。⼀般⽽⾔,多个线程并发同⼀段只有局部变量的代码时,不会出现不同的结果。但是对全局变量 或者静态变量进⾏操作,并且没有锁保护的情况下,容易出现该问题。
重⼊:同⼀个函数被不同的执⾏流调⽤,当前⼀个流程还没有执⾏完,就有其他的执⾏流再次进⼊, 我们称之为重⼊。⼀个函数在重⼊的情况下,运⾏结果不会出现任何不同或者任何问题,则该函数被称为可重⼊函数,否则,是不可重⼊函数。

可重⼊与线程安全联系
• 函数是可重⼊的,那就是线程安全的(其实知道这⼀句话就够了)
• 函数是不可重⼊的,那就不能由多个线程使⽤,有可能引发线程安全问题
• 如果⼀个函数中有全局变量,那么这个函数既不是线程安全也不是可重⼊的。
可重⼊与线程安全区别
可重⼊函数是线程安全函数的⼀种
线程安全不⼀定是可重⼊的,⽽可重⼊函数则⼀定是线程安全的。
如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重⼊函数若锁还
未释放则会产⽣死锁,因此是不可重⼊的。

如果不考虑 信号导致⼀个执⾏流重复进⼊函数 这种重⼊情况,线程安全和重⼊在安全⻆
度不做区分

但是线程安全侧重说明线程访问公共资源的安全情况,表现的是并发线程的特点

可重⼊描述的是⼀个函数是否能被重复进⼊,表⽰的是函数的特点

三.死锁


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

例子理解,
你和小明都各有5毛,去小卖部买棒棒糖,当要1块钱,你让小明把钱给你,小明让你把钱给他,这样你们就陷入了僵局

3.1 4个必要条件

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

3.2 避免死锁

即破坏上面的任一一个条件都可

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

四.stl,智能指针和线程安全

stl的线程不是安全的

原因是, STL 的设计初衷是将性能挖掘到极致, ⽽⼀旦涉及到加锁保证线程安全, 会对性能造成巨⼤的影 响.⽽且对于不同的容器, 加锁⽅式的不同, 性能可能也不同(例如hash表的锁表和锁桶). 因此 STL 默认不是线程安全. 如果需要在多线程环境下使⽤, 往往需要调⽤者⾃⾏保证线程安全
对于 unique_ptr, 由于只是在当前代码块范围内⽣效, 因此不涉及线程安全问题.
对于 shared_ptr, 多个对象需要共⽤⼀个引⽤计数变量, 所以会存在线程安全问题. 但是标准库实现的时候考虑到了这个问题, 基于原⼦操作(CAS)的⽅式保证 shared_ptr 能够⾼效, 原⼦的操作引⽤计数

相关推荐
沉默-_-1 小时前
接收请求:HttpServletRequest的几种用法
前端·servlet·firefox
一尾流莺1 小时前
狼人杀娱乐版型介绍
前端
wuhen_n1 小时前
v-once和v-memo完全指南:告别不必要的渲染,让应用飞起来
前端·javascript·vue.js
ambition202421 小时前
蓝桥杯“水质检测“问题:0-1 BFS算法的完整解析
c语言·数据结构·c++·算法·蓝桥杯·宽度优先
柒.梧.2 小时前
深入理解 HashMap 扩容流程:从 1.7 到 1.8 的演进与细节解析
java
皙然2 小时前
深入解析 Java 中的 final 关键字
java·开发语言·算法
liulilittle2 小时前
解决 liburing 编译时缺失 `linux/time_types.h` 的问题
linux·运维·服务器·ubuntu·shell
云深麋鹿2 小时前
C++ | 手搓一个string类
开发语言·c++·容器
阿里嘎多学长2 小时前
2026-03-15 GitHub 热点项目精选
开发语言·程序员·github·代码托管