设计模式学习——单例模式
引入
单例模式,顾名思义是一种创建单一对象的方式。现实情景中,我们会在某些场景下使用单一对象。比如:
- 一个班级只有一个班主任。
- Windows 在多进程多线程环境下操作文件时,避免多个进程或线程同时操作一个文件,需要通过唯一实例进行处理。
- 设备管理器设计为单例模式,例如电脑有两台打印机,避免同时打印同一个文件。
此次我们将以宝可梦探险队中的任务板为例,尝试搭建基于单例模式的消息队列。
单一性的保证
要确保在整个程序运行过程中只能存在一个单一变量,就要在定义类时删除或隐藏类的构造函数。在C++中,我们需要删除类的移动/复制构造函数,隐藏类的普通构造函数。
1
2
3
4
5
6
7
8
9
10
| class TaskBoard {
public:
TaskBoard(const TaskBoard& obj) = delete;
TaskBoard& operator=(const TaskBoard& obj) = delete;
~TaskBoard() = default;
static TaskBoard* getInstance();
private:
TaskBoard() = default;
static TaskBoard* _instance;
};
|
同时,我们声明公开静态的getInstance()
方法和私有的_instance
指针。这样,其它对象就能使用该方法获取TaskBoard的单一实例。
1
2
3
4
| TaskBoard* TaskBoard::getInstance() {
return TaskBoard::_instance;
}
TaskBoard* TaskBoard::_instance = new TaskBoard;
|
需要注意的是,由于C++中静态成员变量必须在类外部定义和初始化,所以TaskBoard* TaskBoard::_instance = new TaskBoard;这行代码仍然可以在作用域外部访问到该变量。
饿汉/懒汉模式
我们将刚才的getInstance()
方法所代表的模式成为单例模式的饿汉模式。
1
2
3
4
5
6
7
8
9
| /* 饿汉模式
* 类加载时先创建好实例,再对外提供应用
* 避免出现资源竞争
*/
TaskBoard* TaskBoard::getInstance() {
return TaskBoard::_instance;
}
TaskBoard* TaskBoard::_instance = new TaskBoard;
|
与饿汉模式相对的是懒汉模式。
1
2
3
4
5
6
7
8
9
10
11
| /* 懒汉模式
* 在使用getInstance()时创建唯一实例
* 减少内存占用
* 非线程安全,需要特殊处理
*/
TaskBoard* TaskBoard::getInstance() {
if(_instance == nullptr)
_instance = new TaskBoard;
return TaskBoard::_instance;
}
|
不得不提的是,在现代应用程序中懒汉模式减少的内存占用并不显著。因此大多数情况下可考虑直接使用饿汉模式。
懒汉模式的线程安全
容易发现,当多个线程调用懒汉模式下的getInstance()
方法时存在线程隐患。C++中有以下几种方法可以提供懒汉模式下的线程安全保障:
双重检查锁定法(不完全的线程安全)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| /* 懒汉模式下的双重检查锁定法
* 通过两个嵌套的if来判断对象是否为空
* 不完全的线程安全(可能会被指令重排)
*/
TaskBoard* TaskBoard::getInstance() {
if(_instance == nullptr) {
_mutex_.lock();
if(_instance == nullptr)
_instance = new TaskBoard;
_mutex.unlock();
}
return TaskBoard::_instance;
}
|
双重检查锁定法 + 原子变量(完全线程安全,但性能开销大)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| /* 懒汉模式原子变量实现
* 在双重检查锁定的基础上使用原子变量
* 保证处理指令的时候使用默认原子顺序进行顺序原子操作
* 避免指令重排
* 多线程安全但效率低
*/
TaskBoard* TaskBoard::getInstance() {
TaskBoard* board = _atomic_instance.load();
if(board == nullptr) {
std::lock_guard<std::mutex> locker(_mutex); // 使用lock_guard管理锁,无需手动解锁
if(board == nullptr) {
board = new TaskBoard;
_atomic_instance.store(board);
}
}
return board;
}
|
局部静态模式(完全线程安全,需要标准>=C++11)
1
2
3
4
5
6
7
8
9
| /* 懒汉方法使用静态局部变量实现
* 在getInstance()方法中定义了一个局部公告板对象,并且将这个对象作为唯一的单例实例。
* C++11规定如果指令逻辑进入一个未被初始化的声明变量,所有并发执行应当等待该变量完成初始化。
*/
TaskBoard* TaskBoard::getInstance() {
static TaskBoard board;
return &board;
}
|
实现任务板信息队列
在宝可梦不可思议的迷宫:空之探险队中,玩家所处的宝藏镇中有一块公告板。需要帮助的宝可梦可以在公告板上刊登委托,而探险队可以接下委托并进入迷宫完成任务。在这个情境中,我们可以认为这块公告任务板是一个唯一的消息队列。我们可以利用单例模式对其进行建模,模拟两只宝可梦发布任务,三只探险队接收任务并且完成任务的场景。
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
| //TaskBoard.cpp
std::mutex TaskBoard::_mutex;
TaskBoard* TaskBoard::getInstance() {
return TaskBoard::_instance;
}
TaskBoard* TaskBoard::_instance = new TaskBoard;
// 使用饿汉模式
// 即使如此涉及到除了单一实例以外的变量也要注意多线程安全性
bool TaskBoard::isEmpty() const{
std::lock_guard<std::mutex> locker(_mutex);
if(_taskQueue.empty())
return true;
return false;
}
void TaskBoard::popTask() {
std::lock_guard<std::mutex> locker(_mutex);
if(!_taskQueue.empty()) _taskQueue.pop();
}
void TaskBoard::addTask(const int id) {
std::lock_guard<std::mutex> locker(_mutex);
_taskQueue.push(id);
}
int TaskBoard::getTask() const{
std::lock_guard<std::mutex> locker(_mutex);
if(!_taskQueue.empty()) return _taskQueue.front();
return -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
28
29
30
31
32
33
| //ExpeditionTeam.cpp
ExpeditionTeam::ExpeditionTeam(const std::string& name, int solveTime) : _name(name), _solveTime(solveTime) {
_taskBoard = TaskBoard::getInstance();
};
void ExpeditionTeam::startWorking() {
while(!_taskBoard->isEmpty()) {
getTask();
solveTask();
}
}
void ExpeditionTeam::getTask() {
if(!_taskBoard->isEmpty()) {
std::cout << std::endl << "Task ID: " << _taskBoard->getTask() << " taken by " << "Team " << _name << std::endl;
_taskBoard->popTask();
}
}
void ExpeditionTeam::solveTask() {
std::this_thread::sleep_for(std::chrono::seconds(_solveTime));
std::cout << std::endl << "Task solved by Team " << _name << " with " << _solveTime << "sec." <<std::endl;
}
//HelpSeeker.cpp
HelpSeeker::HelpSeeker(const std::string &name) : _name(name){
_taskBoard = TaskBoard::getInstance();
}
void HelpSeeker::sendTask(int id) const {
_taskBoard->addTask(id);
std::cout << std::endl << "Pokemon " << _name << " added a request! ID: " << id << std::endl;
}
|
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
35
| //TreasureTown.cpp
int main() {
ExpeditionTeam team1("LoveFromEevees", 4);
ExpeditionTeam team2("RazorWind", 3);
ExpeditionTeam team3("Skull", 5);
HelpSeeker pm1("Mawile");
HelpSeeker pm2("Poochyena");
std::thread producer_th1([&] {
for (int i = 0; i < 50; i++) {
pm1.sendTask(randid());
std::this_thread::sleep_for(std::chrono::seconds(1));
}
});
std::thread producer_th2([&] {
for (int i = 0; i < 50; i++) {
pm2.sendTask(randid());
std::this_thread::sleep_for(std::chrono::seconds(1));
}
});
std::this_thread::sleep_for(std::chrono::seconds(3));
std::thread consumer_th1(&ExpeditionTeam::startWorking, &team1);
std::thread consumer_th2(&ExpeditionTeam::startWorking, &team2);
std::thread consumer_th3(&ExpeditionTeam::startWorking, &team3);
producer_th1.join();
producer_th2.join();
consumer_th1.join();
consumer_th2.join();
consumer_th3.join();
return 0;
}
|
可以在我的Github仓库中找到完整代码。
总结
在一个项目中,全局范围内,某个类的实例有且仅有一个,通过这个唯一实例向其他模块提供数据的全局访问,这种模式就叫单例模式。单例模式的典型应用就是任务队列。
- 优点:内存中只有一个实例,减少内存开销,尤其是频繁创建和销毁实例时(如管理学院首页页面缓存);避免资源的多重占用(如写文件操作)。
- 缺点:没有接口,不能继承;与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心实例化方式。