设计模式——单例模式
饿汉模式:
定义类的时候就创建单例对象;
在多线程下,饿汉模式没有线程安全问题(多线程可以同时访问单例对象);
#include <iostream>
#include <string>
using namespace std;
// 饿汉模式 -> 定义类的时候创建单例对象
// 定义一个单例模式的任务队列
class TaskQueue {
public:
TaskQueue(const TaskQueue& t) = delete;
TaskQueue& operator=(const TaskQueue& t) = delete;
static TaskQueue* getInstance() {
return m_taskQ;
}
void print() {
cout << "我是单例对象的一个成员函数" << endl;
}
private:
TaskQueue() = default;
static TaskQueue* m_taskQ; // 静态成员变量,类内声明,类外初始化
};
TaskQueue* TaskQueue::m_taskQ = new TaskQueue; // 饿汉模式
int main() {
TaskQueue* taskQ = TaskQueue::getInstance();
taskQ->print();
return 0;
}
res:
懒汉模式:
使用的时候创建对象的实例;
多线程有安全问题(每个线程都会new一个对象,与单例模式的定义是相悖的);
与饿汉模式对比:节省内存空间(不需要时不占用内存);
#include <iostream>
#include <string>
using namespace std;
// 懒汉模式 -> 什么时候使用这个单例对象,在使用的时候再去创建实例
class TaskQueue {
public:
TaskQueue(const TaskQueue& t) = delete;
TaskQueue& operator=(const TaskQueue& t) = delete;
static TaskQueue* getInstance() { // 与饿汉模式有区别的地方,使用的时候才创建实例
if (m_taskQ == nullptr) {
m_taskQ = new TaskQueue;
}
return m_taskQ;
}
void print() {
cout << "我是单例对象的一个成员函数" << endl;
}
private:
TaskQueue() = default;
static TaskQueue* m_taskQ; // 静态成员变量,类内声明,类外初始化
};
TaskQueue* TaskQueue::m_taskQ = nullptr; // 懒汉模式,初始化静态成员变量为空
int main() {
TaskQueue* taskQ = TaskQueue::getInstance();
taskQ->print();
return 0;
}
res:
懒汉模式 多线程安全问题 解决方案:加互斥锁
加了互斥锁之后的问题:线程顺序访问单例对象,不能同时访问,执行效率低
#include <iostream>
#include <mutex> // ★★★用互斥锁要包含此头文件
using namespace std;
// 懒汉模式 -> 什么时候使用这个单例对象,在使用的时候再去创建实例
class TaskQueue {
public:
TaskQueue(const TaskQueue& t) = delete;
TaskQueue& operator=(const TaskQueue& t) = delete;
static TaskQueue* getInstance() { // 与饿汉模式有区别的地方,使用的时候才创建实例
m_mutex.lock(); // ★★★调用互斥锁的lock方法
if (m_taskQ == nullptr) {
m_taskQ = new TaskQueue;
}
m_mutex.unlock(); // ★★★
return m_taskQ;
}
void print() {
cout << "我是单例对象的一个成员函数" << endl;
}
private:
TaskQueue() = default;
static TaskQueue* m_taskQ; // 静态成员变量,类内声明,类外初始化
static mutex m_mutex; // ★★★在静态成员函数里只能访问静态成员变量,所以把互斥锁定义为静态成员变量
};
TaskQueue* TaskQueue::m_taskQ = nullptr; // 懒汉模式,初始化静态成员变量为空
mutex TaskQueue::m_mutex; // ★★★静态成员 类外初始化
int main() {
TaskQueue* taskQ = TaskQueue::getInstance();
taskQ->print();
return 0;
}
res:
互斥锁的改进:双重检查锁定
static TaskQueue* getInstance() { // 与饿汉模式有区别的地方,使用的时候才创建实例
if (m_taskQ == nullptr) { // ★★★双重检查之一
m_mutex.lock(); // ★★★锁定
if (m_taskQ == nullptr) { // ★★★双重检查之二
m_taskQ = new TaskQueue;
}
m_mutex.unlock();
}
return m_taskQ;
}
原子变量:
双重检查锁定的问题:https://subingwen.cn/design-patterns/singleton/
原因出现在 m_taskQ = new TaskQueue; 在执行过程中,对应的机器指令可能会被重新排序。
正常过程如下:
第一步:分配内存用于保存 TaskQueue 对象。
第二步:在分配的内存中构造一个 TaskQueue 对象(初始化内存)。
第三步:使用 m_taskQ 指针指向分配的内存。
但是重新排列以后执行顺序可能变成这样:
第一步:分配内存用于保存 TaskQueue 对象。
第二步:使用 m_taskQ 指针指向分配的内存。
第三步:在分配的内存中构造一个 TaskQueue 对象(初始化内存)。
假设有A B两个线程,如果线程A按照第二种顺序执行机器指令,执行完前两步后 失去CPU时间片 被挂起了,此时线程B判断 m_taskQ 不为空,但这个指针指向的内存没有被初始化,使用此对象就会出问题。
引入原子变量解决这个问题:
#include <iostream>
#include <mutex>
#include <atomic>
using namespace std;
class TaskQueue {
public:
TaskQueue(const TaskQueue& t) = delete;
TaskQueue& operator=(const TaskQueue& t) = delete;
static TaskQueue* getInstance() {
// 把m_taskQ从原子变量里取出来
// 没有往m_taskQ里存数据之间就加载,取出来的是空指针
TaskQueue* task = m_taskQ.load(); // ★★★
if (task == nullptr) {
m_mutex.lock();
task = m_taskQ.load();
if (task == nullptr) {
task = new TaskQueue;
m_taskQ.store(task); // ★★★
}
m_mutex.unlock();
}
return task;
}
void print() {
cout << "我是单例对象的一个成员函数" << endl;
}
private:
TaskQueue() = default;
// static TaskQueue* m_taskQ;
static mutex m_mutex;
static atomic<TaskQueue*>m_taskQ; // ★★★原子变量需要在静态函数里使用,所以把原子变量声明为静态变量;在原子变量里管理的是一个TaskQueue类型的指针(TaskQueue*)
};
// TaskQueue* TaskQueue::m_taskQ = nullptr;
atomic<TaskQueue*> TaskQueue::m_taskQ; // ★★★在类外声明就行了; 如果要往原子变量里放数据,调用store方法; 如果取数据,调用load方法
mutex TaskQueue::m_mutex;
(最简单的处理方式)
懒汉模式下,另外一种解决多线程安全的方法:使用静态局部对象(前提条件:编译器支持C++11)
因为C++11有如下规定:如果指令逻辑进入一个未被初始化的声明变量,所有并发执行应当等待该变量完成初始化
class TaskQueue {
public:
TaskQueue(const TaskQueue& t) = delete;
TaskQueue& operator=(const TaskQueue& t) = delete;
static TaskQueue* getInstance() {
static TaskQueue task; // 静态局部变量的内存在全局区,当前执行的应用程序关闭时 才被析构; 程序运行期间一直存在; 每次调用getInstance,访问的都是同一块内存区域
return &task; // 因为最后返回的是指针,task是TaskQueue类型的对象,所以对task进行取地址操作
}
void print() {
cout << "我是单例对象的一个成员函数" << endl;
}
private:
TaskQueue() = default;
};
一个简单的任务队列的例子:
#include <iostream>
#include <queue>
#include <mutex>
#include <thread> // 多线程对应的头文件
using namespace std;
// 替巴基写一个任务队列
// 饿汉模式
class TaskQueue {
public:
TaskQueue(const TaskQueue& t) = delete;
TaskQueue& operator=(const TaskQueue& t) = delete;
static TaskQueue* getInstance() {
return m_taskQ;
}
void print() {
cout << "我是单例对象的一个成员函数" << endl;
}
// 判断任务队列是否为空
bool isEmpty() {
// 通过lock_guard关键字管理互斥锁
lock_guard<mutex>locker(m_mutex); // 此时加锁,析构时解锁(isEmpty()函数运行完,这个局部变量locker就自动析构了,避免了死锁的问题)
bool flag = m_data.empty();
return flag;
}
// 添加任务
void addTask(int node) { // 任务队列里存储的是什么类型,添加的任务就要是什么类型
lock_guard<mutex>locker(m_mutex);
m_data.push(node);
}
// 删除任务(删除队头任务)
bool popTask() {
lock_guard<mutex>locker(m_mutex);
if (m_data.empty()) {
return false;
}
m_data.pop();
return true;
}
// 取出一个任务(不删除任务)
int taskTask() {
lock_guard<mutex>locker(m_mutex);
if (m_data.empty()) {
return false;
}
int data = m_data.front();
return data;
}
private:
TaskQueue() = default;
static TaskQueue* m_taskQ;
// 定义任务队列(此成员变量没有必要定义为静态,它属于那个唯一的实例)
queue<int>m_data;
mutex m_mutex;
};
TaskQueue* TaskQueue::m_taskQ = new TaskQueue;
int main() {
TaskQueue* taskQ = TaskQueue::getInstance();
// 生产者
// thread t1([=]() { ... }) 创建了一个新线程 t1,这个线程会执行lambda表达式中的代码块,而且使用捕获列表 [=] 捕获了外部作用域的变量值
// [=]() 是一个lambda表达式的开头,[] 是lambda表达式的捕获列表。在这个情况下,使用= 表示捕获外部作用域中的所有变量值,使得在lambda内部可以访问这些变量
// { ... } 是lambda表达式的主体,它包含了在新线程中要执行的代码
thread t1([=]() {
for (int i = 0; i < 10; i++) {
taskQ->addTask(i + 100);
cout << "+++ push data:" << i + 100 << " threadID:" << this_thread::get_id() << endl; // 输出添加的数据和当前的线程id
// 让当前线程休眠一段时间
this_thread::sleep_for(chrono::milliseconds(500)); // 休眠500毫秒
}
});
// 消费者
thread t2([=]() {
this_thread::sleep_for(chrono::milliseconds(100));
while (!taskQ->isEmpty()) {
int num = taskQ->taskTask(); // 取任务
cout << "+++ take data:" << num << " threadID:" << this_thread::get_id() << endl;
taskQ->popTask(); // 删除任务
this_thread::sleep_for(chrono::milliseconds(1000));
}
});
// t1.join(): 这行代码会阻塞当前线程(通常是主线程),直到线程 t1 完成执行为止。换句话说,当执行到 t1.join() 时,主线程会等待线程 t1 完成其任务后再继续执行主线程的后续代码。这是一种等待线程执行完毕的方式,确保主线程不会在子线程还在运行时就结束。
// 线程 t1、线程 t2 和主线程是三个并行运行的执行单元。每个线程都独立地执行自己的任务。通过使用 t1.join() 和 t2.join(),主线程会等待子线程执行完毕后再继续执行
t1.join();
t2.join();
return 0;
}
res:
★★★原文链接★★★:https://subingwen.cn/design-patterns/singleton/
(〃>_<;〃)(〃>_<;〃)(〃>_<;〃)