单例模式
在一个项目中,全局范围内,某个类的实例有且仅有一个,通过这个唯一的实例向其他模块提供数据的全局访问,这种模式就叫单例模式。
为什么不使用全局变量而是使用单例代替全局变量?
直接使用全局变量会破坏类的封装,访问权限不受限制,任意位置的任意模块都可以对全局变量进行读写操作,在一处对全局变量进行修改可能会影响到其他模块正常运行
单例模式的类的创建
C++中创建一个实例对象是通过new来操作,其本质上是调用了这个类的构造函数
三种构造函数
- 默认构造函数:创建一个新的对象,单例模式中需要处理掉
- 拷贝构造函数:根据已有对象拷贝出一个新的对象,单例模式中需要处理掉
- 移动构造函数:参数为本类右值引用^右值引用的构造函数(&&),转移右值对象的资源, 因其本质是资源的转移(对象A的资源转移给对象B),还是只有一份实例,所以单例模式中移动构造函数可以存在。
单例模式中对默认构造函数和拷贝构造函数的处理
1.将默认构造函数和拷贝构造函数的权限设置为私有的
1 2 3 4 5 6
| class TaskQueue { private: TaskQueue(){} TaskQueue(const TaskQueue& t){} };
|
在定义这两个构造函数时需要进行空实现,或者也可以使用C++11的新特性(构造函数 = default),= default就是使用这两个构造函数的默认行为,这两个构造函数的默认行为我们在这里没有修改,只是将它的访问权限设为私有的。
1 2 3 4 5 6
| class TaskQueue { private: TaskQueue() = default; TaskQueue(const TaskQueue& t) = default; };
|
2.可以直接将这两个构造函数删除( = delete)
1 2 3 4 5 6
| class TaskQueue { public: TaskQueue() = delete; TaskQueue(const TaskQueue& t) = delete; };
|
创建了一个新的类之后除了提供构造函数,还提供了一个析构函数和两个运算符重载,一个是拷贝赋值操作符重载(对应拷贝构造函数),一个是移动赋值操作符重载(对应移动构造函数),需要将拷贝赋值操作符重载也delete掉。
1 2 3 4 5 6 7
| class TaskQueue { public: TaskQueue() = delete; TaskQueue(const TaskQueue& t) = delete; TaskQueue& operator=(const TaskQueue& t) = delete; };
|
得到这个类的实例
经过这三步delete,我们已经无法在外部创建任何对象(不能通过new来得到),就只能通过类名来得到,这个通过类名得到的对象必须是静态的,所以需要定义静态的成员和方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| class TaskQueue { public: TaskQueue() = delete; TaskQueue(const TaskQueue& t) = delete; TaskQueue& operator=(const TaskQueue& t) = delete; static TaskQueue* getInstance() { return m_taskQ; } private: static TaskQueue* m_taskQ; };
|
为什么成员和方法都必须是静态的?
因为无法new出对象,只能通过 ==类名::方法()== 获取对象,这个方法必须是静态的,因为调用时还没有对象存在,无法使用非静态方法,而静态方法属于类本身(没有this指针),无需对象即可调用。静态方法只能访问静态成员,而且将m_taskQ设为静态的,也可以保证全局只有一个实例。
给静态指针做初始化
由于是静态的必须在类外做初始化,同时需将禁用默认构造函数改为显示保留(=delete 改为 =default)
错误写法:static TaskQueue* m_taskQ = nullptr;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| class TaskQueue { public: TaskQueue(const TaskQueue& t) = delete; TaskQueue& operator=(const TaskQueue& t) = delete; static TaskQueue* getInstance() { return m_taskQ; } private: TaskQueue() = default; static TaskQueue* m_taskQ; }; TaskQueue* TaskQueue::m_taskQ = new TaskQueue;
|
=delete和=default的区别
=delete:禁用编译器自动生成的函数,适用于禁止某些操作(如单例模式禁止拷贝,禁止默认构造)。
=default:显式要求编译器生成默认构造函数,适用于保留默认函数的同时,自定义其他构造函数。
测试代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| #include<iostream> 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(); }
|
输出结果 :单例对象成员函数的调用!
饿汉模式和懒汉模式
单例模式有两种模式饿汉模式和懒汉模式
1.饿汉模式
- 饿汉模式是定义类的时候创建单例对象(上述单例模式测试的例子就是饿汉模式)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| class TaskQueue { public: TaskQueue(const TaskQueue& t) = delete; TaskQueue& operator=(const TaskQueue& t) = delete; static TaskQueue* getInstance() { return m_taskQ; } private: TaskQueue() = default; static TaskQueue* m_taskQ; };
TaskQueue* TaskQueue::m_taskQ = new TaskQueue;
|
2.懒汉模式
- 懒汉模式是神么时候使用这个单例对象,在使用的时侯再去创建对应的实例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| 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; } private: TaskQueue() = default; static TaskQueue* m_taskQ; }; TaskQueue* TaskQueue::m_taskQ = nullptr;
|
饿汉模式和懒汉模式的对比
| 对比 |
饿汉模式 |
懒汉模式 |
| 创建时机 |
程序启动时 |
首次调用getInstance()时 |
| 多线程 |
线程安全 |
需手动加锁 |
| 资源占用 |
启动时即占用资源,可能浪费 |
使用这个对象时才创建这个对象的实例,节约资源(适用于嵌入式等内存有限的情况) |
懒汉模式下线程安全问题的解决
1.使用双重检查锁定线程安全问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| #include<mutex> class TaskQueue { public: TaskQueue(const TaskQueue& t) = delete; TaskQueue& operator=(const TaskQueue& t) = delete; static TaskQueue* getInstance() { if(m_taskQ == nullptr) { m_mutex.lock(); if(m_taskQ == nullptr) { m_taskQ = new TaskQueue; } m_mutex.unlock(); } return m_taskQ; } private: TaskQueue() = default; static TaskQueue* m_taskQ; static mutex m_mutex; }; TaskQueue* TaskQueue::m_taskQ = nullptr; mutex TaskQueue::m_mutex;
|
但实际上m_task = new TaskQueue;在执行过程中==对应的机器指令可能会被重新排序==。正常过程如下:
- 第一步:分配内存用于保存TaskQueue对象。
- 第二步:在分配的内存中构造一个TaskQueue对象(初始化内存)。
- 第三步:使用m_taskQ指针指向分配的内存。
但是被重新排序以后执行顺序可能会变成这样:
- 第一步:分配内存用于保存TaskQueue对象。
- 第二步:使用m_taskQ指针指向分配的内存。
- 第三步:在分配的内存中构造一个TaskQueue对象(初始化内存)。
重新排序后的第二步m_taskQ把这块内存保存下来后就不是空指针了,也就是说线程可以直接使用这个指针指向的地址里的对象,但是这个地址里还没有对象(重排后第三步才有),这时某个线程通过这个指针把这块内存地址拿走了,拿走后直接对这块内存里数据进行操作,但这块内存还没有数据,所以程序会崩溃。
解决方法:C++11中引入了原子变量atomic,在底层可以控制机器指令的执行顺序。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| #include<mutex> #include<atomic> class TaskQueue { public: TaskQueue(const TaskQueue& t) = delete; TaskQueue& operator=(const TaskQueue& t) = delete; static TaskQueue* getInstance() { 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; } private: TaskQueue() = default; static mutex m_mutex; static atomic<TaskQueue*> m_taskQ; };
atomic<TaskQueue*> TaskQueue::m_taskQ; mutex TaskQueue::m_mutex;
|
store()方法用来存储对象
load()方法用来加载对象,在没有存储对象时调用会加载出一个空对象
在原子变量中这两个函数在处理指令的时候默认的原子顺序是memory_order_seq_cst(顺序原子操作 - sequentially consistent),使用顺序约束原子操作库,整个函数执行都将保证顺序执行,并且不会出现数据竞态,不足之处就是使用这种方法实现的懒汉模式的单例执行效率更低一些。
2.使用静态的局部对象(需要编译器支持C++11)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| class TaskQueue { public: TaskQueue(const TaskQueue& t) = delete; TaskQueue& operator=(const TaskQueue& t) = delete; static TaskQueue* getInstance() { static TaskQueue task; return &task; } private: TaskQueue() = default; }; 对象
|
- 创建的对象必须是静态的,否则出了
getInstance()就被析构了
- 这种方法可行是因为C++11规定:
如果指令逻辑进入一个未被初始化的声明变量,所有并发执行应当等待该变量完成初始化。