单例模式是什么
在一个项目中,全局范围内,某个类的实例有且仅有一个(只能new一次),通过这个唯一的实例向其他模块提供数据的全局访问,这种模式就叫单例模式。单例模式的典型应用就是任务队列。
为什么要使用单例模式
单例模式充当的就是一个全局变量,为什么不直接使用全局变量呢,因为全局变量破坏类的封装,而且不受保护,访问不受限制。
饿汉模式
饿汉模式是在类加载时进行实例化的。
cpp
#include<iostream>
using namespace std;
class TaskQueue
{
public:
TaskQueue(const TaskQueue& obj) = delete;//禁用拷贝构造
TaskQueue& operator = (const TaskQueue& obj) = delete;//禁用赋值构造
static TaskQueue* getInstance() //获取单例的方法
{
cout << "我是一个饿汉模式单例" << endl;
return m_taskQ;
}
private:
TaskQueue() = default; //无参构造
static TaskQueue* m_taskQ; //静态成员需要在类外定义
};
TaskQueue* TaskQueue::m_taskQ = new TaskQueue;//new一个实例;
int main()
{
TaskQueue* obj = TaskQueue::getInstance();
}
懒汉模式
懒汉模式是在需要使用的时候再进行实例化
cpp
#include<iostream>
using namespace std;
class TaskQueue
{
public:
TaskQueue(const TaskQueue& obj) = delete;//禁用拷贝构造
TaskQueue& operator = (const TaskQueue& obj) = delete;//禁用赋值构造
static TaskQueue* getInstance() //获取单例的方法
{
if (nullptr == m_taskQ)
{
m_taskQ = new TaskQueue;
}
return m_taskQ;
}
private:
TaskQueue() = default; //无参构造
static TaskQueue* m_taskQ; //静态成员需要在类外定义
};
TaskQueue* TaskQueue::m_taskQ = nullptr;
int main()
{
TaskQueue* obj = TaskQueue::getInstance();
}
在调用**getInstance()**函数获取单例对象的时候,如果在单线程情况下是没有什么问题的,如果是多个线程,调用这个函数去访问单例对象就有问题了。假设有三个线程同时执行了getInstance()函数,在这个函数内部每个线程都会new出一个实例对象。此时,这个任务队列类的实例对象不是一个而是3个,很显然这与单例模式的定义是相悖的。
线程安全问题
对于饿汉模式来说是没有线程安全问题的,在这种模式下访问单例对象时,这个对象已经被创建出来了,要解决懒汉模式的线程安全问题,最常用的解决方案就是使用互斥锁,可以将创建单例对象的代码使用互斥锁锁住:
cpp
#include<iostream>
#include<mutex>
using namespace std;
class TaskQueue
{
public:
TaskQueue(const TaskQueue& obj) = delete;//禁用拷贝构造
TaskQueue& operator = (const TaskQueue& obj) = delete;//禁用赋值构造
static TaskQueue* getInstance() //获取单例的方法
{
m_mutex.lock();
if (nullptr == m_taskQ)
{
cout << "我加了互斥锁" << endl;
m_taskQ = new TaskQueue;
}
m_mutex.unlock();
return m_taskQ;
}
private:
TaskQueue() = default; //无参构造
static TaskQueue* m_taskQ; //静态成员需要在类外定义
static mutex m_mutex; //定义为静态的,因为静态函数只能使用静态变量
};
mutex TaskQueue::m_mutex;
TaskQueue* TaskQueue::m_taskQ = nullptr;
int main()
{
TaskQueue* obj = TaskQueue::getInstance();
}
在上面代码的10~13 行这个代码块被互斥锁锁住了,也就意味着不论有多少个线程,同时执行这个代码块的线程只能是一个(相当于是严重限行了,在重负载情况下,可能导致响应缓慢)。我们可以将代码再优化一下:
cpp
#include<iostream>
#include<mutex>
using namespace std;
class TaskQueue
{
public:
TaskQueue(const TaskQueue& obj) = delete;//禁用拷贝构造
TaskQueue& operator = (const TaskQueue& obj) = delete;//禁用赋值构造
static TaskQueue* getInstance() //获取单例的方法
{
if (nullptr == m_taskQ)
{
m_mutex.lock();
if (nullptr == m_taskQ)
{
cout << "我加了互斥锁" << endl;
m_taskQ = new TaskQueue;
}
m_mutex.unlock();
}
return m_taskQ;
}
private:
TaskQueue() = default; //无参构造
static TaskQueue* m_taskQ; //静态成员需要在类外定义
static mutex m_mutex;
};
mutex TaskQueue::m_mutex;
TaskQueue* TaskQueue::m_taskQ = nullptr;
int main()
{
TaskQueue* obj = TaskQueue::getInstance();
}
双重检查锁定问题
cpp
#include<iostream>
#include<mutex>
#include<atomic>
using namespace std;
class TaskQueue
{
public:
TaskQueue(const TaskQueue& obj) = delete;//禁用拷贝构造
TaskQueue& operator = (const TaskQueue& obj) = delete;//禁用赋值构造
static TaskQueue* getInstance() //获取单例的方法
{
TaskQueue* taskQ = m_taskQ.load(); //取出来单例的值
if (nullptr == taskQ)
{
m_mutex.lock();
taskQ = m_taskQ.load();
if (nullptr == taskQ)
{
cout << "我加了原子" << endl;
taskQ = new TaskQueue;
m_taskQ.store(taskQ); //保存到原子变量中
}
m_mutex.unlock();
}
return m_taskQ.load();
}
private:
TaskQueue() = default; //无参构造
static atomic<TaskQueue*>m_taskQ; //定义为原子变量
static mutex m_mutex;
};
mutex TaskQueue::m_mutex;
atomic<TaskQueue*> TaskQueue::m_taskQ;
int main()
{
TaskQueue* obj = TaskQueue::getInstance();
}
对于m_taskQ = new TaskQueue
这行代码来说,我们期望的执行机器指令执行顺序是:
(1)分配用来保存TaskQueue对象的内存;
(2)在分配好的内存中构造一个TaskQueue对象(即初始化内存);
(3)使用m_taskQ指针指向分配的内存。
但是对多线程来说,机器指令可能会被重新排列;即:
(1)分配内存用于保存 TaskQueue 对象。
(2)使用 m_taskQ 指针指向分配的内存。
(3)在分配的内存中构造一个 TaskQueue 对象(初始化内存)。
这样重排序并不影响单线程的执行结果,但是在多线程中就会出问题。如果线程A按照第二种顺序执行机器指令,执行完前两步之后失去CPU时间片被挂起了,此时线程B在第3行处进行指针判断的时候m_taskQ 指针是不为空的,但这个指针指向的内存却没有被初始化,最后线程 B 使用了一个没有被初始化的队列对象就出问题了(出现这种情况是概率问题,需要反复的大量测试问题才可能会出现)。
在C++11中引入了原子变量atomic,通过原子变量可以实现一种更安全的懒汉模式的单例,代码如下:
cpp
#include<iostream>
#include<mutex>
#include<atomic>
using namespace std;
class TaskQueue
{
public:
TaskQueue(const TaskQueue& obj) = delete;//禁用拷贝构造
TaskQueue& operator = (const TaskQueue& obj) = delete;//禁用赋值构造
static TaskQueue* getInstance() //获取单例的方法
{
TaskQueue* taskQ = m_taskQ.load(); //取出来单例的值
if (nullptr == taskQ)
{
m_mutex.lock();
taskQ = m_taskQ.load();
if (nullptr == taskQ)
{
cout << "我加了原子" << endl;
taskQ = new TaskQueue;
m_taskQ.store(taskQ); //保存到原子变量中
}
m_mutex.unlock();
}
return m_taskQ.load();
}
private:
TaskQueue() = default; //无参构造
static atomic<TaskQueue*>m_taskQ; //定义为原子变量
static mutex m_mutex;
};
mutex TaskQueue::m_mutex;
atomic<TaskQueue*> TaskQueue::m_taskQ;
int main()
{
TaskQueue* obj = TaskQueue::getInstance();
}
使用局部静态
c++11新特性有如下规定:如果指令逻辑进入一个未被初始化的声明标量,所有并发执行应当等待该变量完成初始化。
cpp
#include<iostream>
using namespace std;
class TaskQueue
{
public:
TaskQueue(const TaskQueue& obj) = delete;//禁用拷贝构造
TaskQueue& operator = (const TaskQueue& obj) = delete;//禁用赋值构造
static TaskQueue* getInstance() //获取单例的方法
{
static TaskQueue m_taskQ; //未被初始化
return &m_taskQ;
}
void print()
{
cout << "hello, world!!!" << endl;
}
private:
TaskQueue() = default; //无参构造
};
int main()
{
TaskQueue* obj = TaskQueue::getInstance();
obj->print();
}
饿汉模式和懒汉模式的区别
懒汉模式的缺点是在创建实例对象的时候有安全问题,但这样可以减少内存的浪费(如果用不到就不去申请内存了)。饿汉模式则相反,在我们不需要这个实例对象的时候,它已经被创建出来,占用了一块内存。对于现在的计算机而言,内存容量都是足够大的,这个缺陷可以被无视。