单例模式笔记总结
首先它都叫单例模式了,那它的特点就是全局只有唯一一个实例。所以这个设计模式的核心就是围绕着如何维护全局唯一实例。
如何保证全局只有一个实例
不妨先想想有什么办法可以实例化对象,再将路堵死
- 首先想到的就是构造函数
- 不能在类外使用构造函数构造一个实例
- 其次就是拷贝构造函数
- 拷贝构造函数就是用一个旧实例初始化一个新实例,所以也不能在类外使用
- 最后就是拷贝赋值操作符
- 拷贝赋值操作会将右值浅拷贝再赋值
以上就是它可以造出实例的办法了,那么只需要在类中设为私有或禁用 即可
**注意: 构造函数不能禁用,只能私有化。否则一个实例都构造不出来
- 禁用
c++
class TaskQueue{
public:
TaskQueue(const TaskQueue& t) = delete;
TaskQueue& operator= (const TaskQueue& t) = delete;
private:
TaskQueue() = default;
};
2.私有化
c++
class TaskQueue{
public:
private:
TaskQueue() = default;
TaskQueue(const TaskQueue& t) = default;
TaskQueue& operator= (const TaskQueue& t) = default;
};
现在在类外是无法实例化对象了,而我们又必须要一个实例化对象,只能在类内声明了。
- 那现在考虑一个问题:
- 用普通的成员变量可以吗?
- 答案是否定的。因为如果是普通的成员变量,那怎么拿到这唯一的实例呢,通过成员函数吗?
- 显然是不行的(类外无法构造出实例了,用不了成员函数)。
- 那有什么函数是不用实例化对象就可以调用的呢?
- 显然只有静态函数了,那普通成员变量在静态函数中能访问到吗?
- 显然也是不行的,静态函数里只能访问静态成员变量。
综上我们总结出了:静态成员+静态函数
懒汉模式与饿汉模式
饿汉模式
饿汉模式就是在类加载时就初始化实例,通过静态函数返回实例。
- 为什么叫饿汉模式呢?因为在类加载时就初始化了,但不知道什么时候会用它,所以它一直保持"饥饿"状态。(我是这么理解的)
c++
class TaskQueue{
public:
static TaskQueue* getInstance(){
return m_tasks;
}
private:
TaskQueue() = default;
TaskQueue(const TaskQueue& t) = default;
TaskQueue& operator= (const TaskQueue& t) = default;
static TaskQueue* m_tasks;
};
TaskQueue* TaskQueue::m_tasks = new TaskQueue();
懒汉模式
懒汉模式就是第一次通过静态函数返回实例时才初始化。
- 为什么叫懒汉模式呢?因为它将初始化拖到了使用时才进行,很懒哈,临时抱佛脚。
c++
class TaskQueue{
public:
static TaskQueue* getInstance(){
if(m_tasks==nullptr){
m_tasks = new TaskQueue();
}
return m_tasks;
}
private:
TaskQueue() = default;
TaskQueue(const TaskQueue& t) = default;
TaskQueue& operator= (const TaskQueue& t) = default;
static TaskQueue* m_tasks;
};
TaskQueue* TaskQueue::m_tasks = nullptr;
懒汉模式下多线程的问题
**在多线程下懒汉模式有重复构造实例的隐患
- 如果现在有两个线程同时获取实例,两个线程同时判断
m_tasks==nullptr,都进入了构造逻辑,生成了两个实例
那怎么办呢?加锁
c++
class TaskQueue{
public:
static TaskQueue* getInstance(){
{
lock_guard<mutex> lg(m_mutex);
if (m_tasks == nullptr) {
m_tasks = new TaskQueue();
}
}
return m_tasks;
}
private:
TaskQueue() = default;
TaskQueue(const TaskQueue& t) = default;
TaskQueue& operator= (const TaskQueue& t) = default;
static TaskQueue* m_tasks;
static mutex m_mutex;
};
TaskQueue* TaskQueue::m_tasks = nullptr;
mutex TaskQueue::m_mutex;
如果是按照上面的情况加锁,每次访问实例都要等待资源,效率太慢。其实只有初始化时才需要上锁,所以我们可以判断:**如果已经初始化了那就可以不用等待资源直接返回实例。
c++
class TaskQueue{
public:
static TaskQueue* getInstance() {
if(m_tasks==nullptr) {
{
lock_guard<mutex> lg(m_mutex);
if (m_tasks == nullptr) {
m_tasks = new TaskQueue();
}
}
}
return m_tasks;
}
private:
TaskQueue() = default;
TaskQueue(const TaskQueue& t) = default;
TaskQueue& operator= (const TaskQueue& t) = default;
static TaskQueue* m_tasks;
static mutex m_mutex;
};
TaskQueue* TaskQueue::m_tasks = nullptr;
mutex TaskQueue::m_mutex;
**注意:内层还是需要if再判断一次的,如果现在有两个线程同时获取实例,两个线程同时判断m_tasks==nullptr,都进入了构造逻辑,就会生成了两个实例
问题来了,现在就安全了吗?
- 看起来是天衣无缝,但是
m_tasks = new TaskQueue();在底层正常会按顺序执行:- 分配内存
- 初始化对象
- 赋值操作
- 但是编译器为了优化会进行**指令重排序
- 分配内存
- 赋值操作
- 初始化对象
现在有一个场景:两个线程访问实例,其中一个线程走到m_tasks = new TaskQueue();遇到了指令重排,先赋值没有初始化对象,那么另外一个线程发现指针不为nullptr,直接返回了一个未知对象。
解决方法:c++11中的atomic原子变量,它默认固定了指令的顺序(当然也可以通过load修改顺序)
- 分配内存
- 初始化对象
- 赋值操作
c++
class TaskQueue{
public:
static TaskQueue* getInstance() {
if(m_tasks==nullptr) {
{
lock_guard<mutex> lg(m_mutex);
if (m_tasks == nullptr) {
m_tasks = new TaskQueue();
}
}
}
return m_tasks;
}
private:
TaskQueue() = default;
TaskQueue(const TaskQueue& t) = default;
TaskQueue& operator= (const TaskQueue& t) = default;
static atomic<TaskQueue*> m_tasks;
static mutex m_mutex;
};
atomic<TaskQueue*> TaskQueue::m_tasks = nullptr;
mutex TaskQueue::m_mutex;
局部静态对象实例
最轮椅的来了。没什么好解释的。
缺点:
- 只能初始化一次
- 只有等程序退出才会销毁对象内存
优点: - 代码简洁
c++
class TaskQueue{
public:
static TaskQueue* getInstance() {
static TaskQueue tasks;
return &tasks;
}
private:
TaskQueue() = default;
TaskQueue(const TaskQueue& t) = default;
TaskQueue& operator= (const TaskQueue& t) = default;
};