“天上地下,唯我独尊” —— 单例模式
前言
你能在电脑上调出两个 WIndows 任务管理器吗?
假设能,如果两个管理器显示的数据相同,那又何必存在两个呢?
如果两个管理器显示的数据不同,那我应该相信哪一个呢?
试试看,应该有且仅有一个吧?一个系统里有且仅有一个 Windows 任务管理器实例供外界访问。如何保证系统里有且仅有一个实例对象呢?并且能够供外界访问?你可以在系统里定义一个统一的全局变量,但这并不能防止创建多个多个对象(想一想,为什么?)这就是单例模式的典型应用。
对于一个软件系统中的某些类来说,只有一个实例很重要。假设 Windows 系统上可以同时调出两个 Windows 任务管理器,这两个任务管理器显示的都是同样的信息,那势必会造成内存资源的浪费;如果两个任务管理器显示的是不同的信息,这也给用户带来了困惑,到底哪一个才是真实的状态?
1. 单例模式简介
单例模式
确保一个类只有一个实例,并提供一个全局访问点来访问这个唯一实例。
单例模式有3个要点:
- 这个类只有一个实例
- 它必须自己创建实例
- 它必须自己向整个系统提供这个实例
2. 单例模式结构
单例模式结构非常简单,其 UML 图如下所示,只包含一个类,即单例类。为防止创建索格对象,其构造函数必须是私有的(外界不能访问)。另一方面,为了提供一个全局访问点来访问该唯一实例,单例类提供了一个公有方法 getInstance 来返回该实例。
3. 单例模式代码及效果
【注意:下列代码均在 Linux 环境下进行测试】
3.1 单例模式代码及验证
单例模式代码:
#ifndef SINGLETON_H
#define SINGLETON_H
#include <iostream>
#include <mutex>
#include <thread>
using namespace std;
class Singleton{
private:
static Singleton *m_instance;
// 构造函数私有
Singleton(){}
// 拷贝构造和赋值运算符私有
Singleton(const Singleton &) = delete;
Singleton &operator=(const Singleton&) = delete;
public:
static Singleton *getInstance()
{
if(nullptr == m_instance)
{
cout << "创建新的实例" << endl;
m_instance = new Singleton();
}
return m_instance;
}
};
Singleton* Singleton::m_instance = nullptr;
#endif // SINGLETON_H
可以看到,构造函数是私有的,拷贝构造函数和赋值运算符也不能使用,即单例模式对象只能在类内部实例化,这就满足了单例模式的第二个要点(即自己创建实例对象)。同时,实例对象 m_instance 是静态的,也就是全局的。假设客户端实例化了两个 Singleton,但是 m_instance 只有一个(这就满足了单例模式只有一个实例对象)。那第三个要点怎样满足呢?即外界如何获取单例对象?上述代码中定义了一个方法 getInstance() 便是获取单例对象。
下面看看客户端怎样使用?
客户端验证代码:
#include <iostream>
#include "Singleton.h"
using namespace std;
void test1()
{
Singleton *s1 = Singleton::getInstance();
Singleton *s2 = Singleton::getInstance();
}
int main()
{
test1();
return 0;
}
效果如下图:虽然客户端创建了2次实例对象,但是实际上实例只创建了一次。
上述的客户端验证似乎说明上述代码实现了单例模式。的确是实现了,但是这样真的安全吗?试想在多线程环境里,当两个线程(甚至更多线程)同时使用,同样存在创建了多个实例的隐患。这便引入了多线程环境的单例模式。
3.2 多线程环境下测试单例模式
我在Linux环境下模拟了10个线程同时使用该对象,那么会像预期的那样只创建一个实例吗?
客户端测试代码:
#include <iostream>
#include <unistd.h>
#include "Singleton.h"
using namespace std;
static int num = 1;
void *GetInstance(void *arg)
{
Singleton *s1 = Singleton::getInstance();
sleep(5);
printf("线程编号为%d\n", num++);
return NULL;
}
void test2()
{
pthread_t tid[10];
for(int i = 0; i < 10; i++)
{
pthread_create(&tid[i], NULL, GetInstance, NULL);
}
for(int i = 0; i < 10; i++)
{
pthread_join(tid[i], NULL);
}
}
int main()
{
test2();
return 0;
}
一共创建了10个线程,每个线程里面都试图创建一个单例对象。理论上,最终只有第一个线程(死一个被系统调度的线程)才能打印出 “创建新的实例”,然后,运行结果如下:
上述结果不言而喻了,3.1 的单例模式的代码并不是线程安全的。
3.3 线程安全的单例模式的代码实现
如何做到线程安全呢?多线程同步与互斥有多重方法,这里主要介绍互斥量这种方法。
代码如下:
#include <iostream>
#include <pthread.h>
using namespace std;
class Singleton{
private:
static Singleton *m_instance;
static pthread_mutex_t mutex;
// 构造函数私有
Singleton(){}
// 拷贝构造和赋值运算符私有
Singleton(const Singleton &) = delete;
Singleton &operator=(const Singleton&) = delete;
public:
static Singleton *getInstance()
{
if(nullptr == m_instance)
{
// 加锁
pthread_mutex_lock(&mutex);
if(nullptr == m_instance)
{
cout << "创建新的实例" << endl;
m_instance = new Singleton();
pthread_mutex_lock(&mutex);
}
}
return m_instance;
}
};
Singleton* Singleton::m_instance = nullptr;
pthread_mutex_t Singleton::mutex;
客户端代码不变,运行结果如下
加了锁机制后,实现了在多线程的环境下也只会创建一个实例。
但是,你会不会对下列代码有一个疑问?为什么需要判断两次 m_instance 是否是空?
第一次判断:是判断当前是否创建了实例对象,如果没有创建,则先加锁再创建。
第二次判断:是在加锁之后再做一次判断。你是否会觉得这部操作很多余。不,这步必须有,因为有这么一种情况,就是在加锁的过程中,有别的线程刚好就在加锁的时间内创建出了这个实例对象,所以为了避免出现这种情况出现,还需要再判断一次。
4. 单例模式总结
优点:
- 单例模式提供了严格的对唯一实例的创建和访问
- 单例模式的实现可以节省系统资源
缺点 - 如果某个实例负责多重职责但又必须实例唯一,那单例类的职责过多,这违背了单一职责原则
- 多线程下要考虑线程安全机制
- 单例模式没有抽象层,不方便扩展
适用环境: - 系统只需要一个实例对象
- 某个实例允许有一个访问接口