线程安全的单例模式,STL和智能指针

目录

什么是单例模式

什么是设计模式

单例模式的特点

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

饿汉方式实现单例模式

懒汉方式实现单例模式

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

STL,智能指针和线程安全

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

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

其他常见的各种锁


什么是单例模式

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

什么是设计模式

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

单例模式的特点

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

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

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

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

洗完的例子

吃完饭, 立刻洗碗, 这种就是饿汉方式. 因为下一顿吃的时候可以立刻拿着碗就能吃饭.

吃完饭, 先把碗放下, 然后下一顿饭用到这个碗了再洗碗, 就是懒汉方式.

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

饿汉方式实现单例模式

cpp 复制代码
template <typename T> 
class Singleton { 
 static T data; 
public: 
 static T* GetInstance() { 
 return &data; 
 } 
}; 

只要通过 Singleton 这个包装类来使用 T 对象, 则一个进程中只有一个T对象的实例.

懒汉方式实现单例模式

cpp 复制代码
template <typename T> 
class Singleton { 
 static T* inst; 
public: 
 static T* GetInstance() { 
 if (inst == NULL) { 
 inst = new T(); 
 } 
 return inst; 
 } 
}; 

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

第一次调用 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 能够高效, 原子的操作引用计数.

其他常见的各种锁

  • 悲观锁:在每次取数据时,总是担心数据会被其他线程修改,所以会在取数据前先加锁(读锁,写锁,行锁等),当其他线程想要访问数据时,被阻塞挂起。
  • 乐观锁:每次取数据时候,总是乐观的认为数据不会被其他线程修改,因此不上锁。但是在更新数据前, 会判断其他数据在更新前有没有对数据进行修改。主要采用两种方式:版本号机制和CAS操作。
  • CAS操作:当需要更新数据时,判断当前内存值和之前取得的值是否相等。如果相等则用新值更新。若不等则失败,失败则重试,一般是一个自旋的过程,即不断重试。
  • 自旋锁,公平锁,非公平锁?
    1. 自旋锁: 线程获取锁失败时不阻塞,而是循环尝试,适用于短时间持有锁的多核场景。
    1. 公平锁: 锁的获取严格按照请求顺序(FIFO),保证所有线程最终都能获取锁,避免饥饿。
    1. 非公平锁: 线程获取锁时直接竞争,允许插队,牺牲公平性换取更高吞吐量。
相关推荐
计算机安禾4 分钟前
【数据结构与算法】第36篇:排序大总结:稳定性、时间复杂度与适用场景
c语言·数据结构·c++·算法·链表·线性回归·visual studio
unicrom_深圳市由你创科技8 分钟前
做虚拟示波器这种实时波形显示的上位机,用什么语言?
c++·python·c#
无限进步_14 分钟前
【C++】电话号码的字母组合:从有限处理到通用解法
开发语言·c++·ide·windows·git·github·visual studio
JJay.29 分钟前
Android Kotlin 协程使用指南
android·开发语言·kotlin
csbysj202035 分钟前
jQuery 捕获详解
开发语言
C++ 老炮儿的技术栈43 分钟前
GCC编译时无法向/tmp 目录写入临时汇编文件,因为设备空间不足,解决
linux·运维·开发语言·汇编·c++·git·qt
橘颂TA1 小时前
【笔试】算法的暴力美学——牛客 NC213140 :除2!
c++·算法·结构与算法
三道渊1 小时前
进程通信与网络协议
开发语言·数据库·php
白露与泡影1 小时前
Java面试题库及答案解析(2026版)
java·开发语言·面试
wsoz1 小时前
Leetcode普通数组-day5、6
c++·算法·leetcode·数组