👦个人主页:Weraphael
✍🏻作者简介:目前正在学习c++和算法
✈️专栏:Linux
🐋 希望大家多多支持,咱一起进步!😁
如果文章有啥瑕疵,希望大佬指点一二
如果文章对你有帮助的话
欢迎 评论💬 点赞👍🏻 收藏 📂 加关注😍
目录
一、STL线程安全问题
STL
库中的容器是否是线程安全的?
答案:不是。
因为STL
设计的初衷就是将性能挖掘到极致,而加锁和解锁操作势必会影响效率,因为它们引入了额外的开销,比如上下文切换和竞争条件。这些操作会导致线程等待锁释放,从而增加了等待时间和系统的整体开销。
因此STL
中的容器并未考虑线程安全,在之前编写的生产者消费者模型和线程池中,使用了部分STL
容器,如vector
、queue
、string
等,这些都是需要我们自己去加锁和解锁,以确保多线程并发访问时的线程安全问题。
二、智能指针线程安全问题
C++
标准提供的智能指针有三种:unique_ptr
、shared_ptr
、weak_ptr
unique_ptr
: 它是一个独占的智能指针,只在当前代码块范围内生效,即unique_ptr
的作用范围只限于当前作用域。因此在多线程环境下是线程安全的。shared_ptr
:多个对象需要共用一个引用计数变量,所以会存在线程安全问题,但是标准库实现的时候考虑到了这个问题,索性将shared_ptr
对于引用计数的操作设计成了原子性(原子操作CAS
),这意味着在多个线程中shared_ptr
的引用计数操作是线程安全。weak_ptr
:这个就是shared_ptr
的补充,名为弱引用智能指针,具体实现与shared_ptr
紧密相关,因此在某些方面继承了shared_ptr
的特性,即weak_ptr
的引用计数操作也是线程安全。
三、线程安全的单例模式
3.1 什么是单例模式
单例模式是一种设计模式。
什么是设计模式?
IT
行业这么火,卷王特别多!俗话说林子大了啥鸟都有,大佬和菜鸡们两极分化的越来越严重.,为了让小菜鸡们快速成长,于是大佬们针对一些经典的常见的场景(模板),给定了一些对应的解决方案,这个就是设计模式。
回过头来,单例模式的主要目的是确保某个类只有一个实例,即类只能有一个对象。这种模式常用于需要全局控制的场景,如配置管理、数据库连接等。
单例模式的核心特性:某些类只应该具有一个对象(实例)
在很多服务器开发场景中,经常需要让服务器加载很多的数据 (上百GB
) 到内存中进行高效处理和管理,此时此时我们就只用一个单例的类来管理这些数据就行了,因为数据只会加载一次,并且所有请求共享同一个数据实例,避免了重复加载和内存浪费。
3.2 单例模式的简单实现
3.2.1 如何创建单例对象
单例模式的核心特性:某些类只应该具有一个对象(实例)。因此,它们避免类被再次创建出对象的手段是一样的:私有化构造函数和删除拷贝构造函数及赋值操作符。
- 构造函数私有化:防止外部直接创建类的实例。将构造函数声明为私有
private
来实现。
class Singleton
{
private:
Singleton() {
} // 构造函数私有化
public:
// 其他公共成员函数
};
- 删除拷贝构造函数和赋值操作符:避免通过复制已有实例来创建新的实例,从而进一步保证唯一性。
C++98/03
:你需要显式地声明拷贝构造函数和赋值操作符为private
,并且没有定义它们。C++11
及以上:使用delete
关键字来显式地删除拷贝构造函数和赋值操作符。这是一种更简洁和现代的做法。- 以上两种方式任选一种。我后面的代码全部用第二种。
class Singleton
{
private:
// 构造函数私有化
Singleton() {
}
// ====== C++98/03 ========
// 拷贝构造函数声明为 private
Singleton(const Singleton&);
// 赋值操作符声明为 private
Singleton& operator=(const Singleton&);
// ===== C++11及以上 =======
// 删除拷贝构造函数
Singleton(const Singleton&) = delete;
// 删除赋值操作符
Singleton& operator=(const Singleton&) = delete;
public:
};
只要外部无法访问构造函数,那么也就无法构建对象了。
那么问题来了,外部无法访问构造函数了,我们应该如何创建一个单例对象呢?
既然外部受权限约束无法创建对象,但类内可以创建对象。所以,只需要创建一个指向该类对象的静态指针或者一个静态对象的私有成员,又因为该成员变量不能在类的外部直接访问,那么可以通过静态成员函数间接访问和操作单例对象句柄(句柄就是只能使用接口来获取某些资源或对象的抽象标识符),将静态成员变量带出去给外部使用。
注意:静态成员函数不能调用非静态成员函数,也不能直接访问非静态成员变量,因为静态成员函数没有this
指针。它们只能访问类的静态成员变量和静态成员函数。
以下是 【代码模板】
class Singleton
{
private:
// 构造函数私有化
Singleton() {
}
// 删除拷贝构造函数
Singleton(const Singleton&) = delete;
// 删除赋值操作符
Singleton& operator=(const Singleton&) = delete;
public:
// 获取单例对象的句柄
static Singleton* getInstance()
{
if (_ptr == nullptr)
{
_ptr = new Singleton();
}
return _ptr;
}
private:
// 指向单例对象的静态指针
static Singleton* _ptr;
};
// 静态成员变量需要在类外初始化
Singleton *Singleton::_ptr = nullptr;
外部可以直接通过getInstance()
获取单例对象的操作句柄,来调用类中的其他函数。
int main()
{
// Singleton::getInstance() -> 获取单例对象的操作句柄
Singleton::getInstance()->run();
return 0;
}
【程序结果】
除了创建静态单例对象指针外,也可以直接定义一个静态单例对象。需要注意的是:getInstance()
需要返回的也是该静态单例对象的地址,不能返回值。因为如果返回的是类对象值,会调用拷贝构造,但拷贝构造被删除了。
class Singleton
{
private:
// 构造函数私有化
Singleton() {
}
// 删除拷贝构造函数
Singleton(const Singleton &) = delete;
// 删除赋值操作符
Singleton &operator=(const Singleton &) = delete;
public:
// 获取单例对象的句柄
static Singleton *getInstance()
{
return &_ptr;
}
void run()
{
cout << "void run()" << endl;
}
private:
// 静态单例对象
static Singleton _ptr;
};
// 静态成员变量需要在类外初始化
Singleton Singleton::_ptr;
int main()
{
Singleton *s1 = Singleton::getInstance();
s1->run();
return 0;
}
【程序结果】
而单例模式的实现方式有多种,但最常见的有两种:饿汉和懒汉。
3.2.2 饿汉方式实现单例模式
饿汉可以这样理解:吃完饭,立刻洗碗,这种就是饿汉方式。因为下一顿吃的时候可以立刻拿着碗就能吃饭。
回到计算机世界
饿汉模式:程序加载到内存或类加载时就立即自动创建单例对象,即不是依赖于主函数或其他代码显式地创建它。
饿汉模式的单例对象本质就有点像全局变量,在程序加载时,对象就已经创建好了。即饿汉模式的单例对象生命周期随进程。
我们可以用以下代码证明:
#include <iostream>
#include <unistd.h>
using namespace std;
class Singleton
{
public:
Singleton()
: _a(1), _b(1)
{
cout << "a + b = " << _a + _b << endl;
}
private:
int _a;
int _b;
};
Singleton s1;
Singleton s2;
Singleton s3;
int main()
{
sleep(3);
return 0;
}