DesignPattern/06.Singleton/06.Singleton.md

6.1 KiB
Raw Blame History

“天上天下,唯我独尊”——单例模式

你能在电脑上调出两个Windows任务管理器吗
假设能,如果两个管理器显示的数据相同,那何必要存在两个呢?
如果两个管理器显示的数据不同,那我该相信哪一个呢?

试试看应该有且仅有一个吧一个系统里有且仅有一个Windows任务管理器实例供外界访问 。如何保证系统里有且仅有一个实例对象呢?并且能够供外界访问?你可以在系统里定义一个统一的全局变量,但这并不能防止创建多个对象(想一想,为什么?)这就是单例模式的典型应用。

对于一个软件系统中的某些类来说只有一个实例很重要。假设Windows系统上可以同时调出两个Windows任务管理器这两个任务管理器显示的都是同样的信息那势必造成内存资源的浪费如果这两个任务管理器显示的是不同的信息这也给用户带来了困惑到底哪一个才是真实的状态

1.单例模式简介

单例模式定义:

单例模式:
确保一个类只有一个实例,并提供一个全局访问点来访问这个唯一实例。

单例模式有3个要点

  • 这个类只能有一个实例;
  • 它必须自己创建这个实例;
  • 它必须自己向整个系统提供这个实例。

2.单例模式结构

单例模式结构非常简单其UML图如下所示只包含一个类即单例类。为防止创建多个对象其构造函数必须是私有的外界不能访问。另一方面为了提供一个全局访问点来访问该唯一实例单例类提供了一个公有方法getInstance来返回该实例。

avatar

3.单例模式代码及效果

3.1.单例模式代码及验证

单例模式代码:

#ifndef __SINGLETON_H__
#define __SINGLETON_H__
 
#include <iostream>
#include <string.h>
using namespace std;
 
//抽象产品类AbstractBall
class Singleton
{
public:
	static Singleton* getInstance(){
		if (instance == NULL){
			printf("创建新的实例\n");
			instance = new Singleton();
		}
		return instance;
	}
private:
	Singleton(){}
 
	static Singleton* instance;
};
 
Singleton* Singleton::instance = NULL;
 
#endif //__SINGLETON_H__

可以看到构造函数是私有的private即单例模式对象只能在类内部实例化这就满足了单例模式的第二个要点。同时实例对象instance是静态static的也就是全局的假设客户端实例化了两个Singleton但instance只有一个这就满足了第一个要点。那第三个要点如何满足呢即外界如何获取单例对象呢上述代码中定义了一个方法同样也是static的getInstance(). 接下来看看客户端如何使用​。

客户端代码验证:

int main()
{
	Singleton *s1 = Singleton::getInstance();
	Singleton *s2 = Singleton::getInstance();
 
	system("pause");
	return 0;
}

效果如下图: avatar

3.2.多线程环境测试单例模式

上述的客户端验证似乎说明上述代码实现了单例模式。的确是实现了,但是这样真的安全吗?试想在多线程环境里,当两个线程(甚至更多线程)同时使用,同样存在创建了多个实例的隐患。下面的代码是在多线程环境下进行的测试:

/*非线程安全 单例模式*/
#include <process.h>
#include <Windows.h>
#define THREAD_NUM 5
 
unsigned int __stdcall CallSingleton(void *pPM)
{
	Singleton *s = Singleton::getInstance();
	int nThreadNum = *(int *)pPM; 
	Sleep(50);
	printf("线程编号为%d\n", nThreadNum);
	return 0;
}
 
int main()
{
	HANDLE  handle[THREAD_NUM];
 
	//线程编号
	int threadNum = 0;
	while (threadNum < THREAD_NUM)
	{
		handle[threadNum] = (HANDLE)_beginthreadex(NULL, 0, CallSingleton, &threadNum, 0, NULL);
		//等子线程接收到参数时主线程可能改变了这个i的值
		threadNum++;
	}
	//保证子线程已全部运行结束
	WaitForMultipleObjects(THREAD_NUM, handle, TRUE, INFINITE);
 
	system("pause");
	return 0;
}

一共创建了5个线程每个线程里面都试图创建一个单例对象。理论上最终只有第一个线程第一个被系统调度的线程才能打印出“创建新的实例”然而 avatar

上述结果说明3.1的单例模式的代码并不是线程安全的。

3.3.线程安全的单例模式代码实现

如何做到线程安全呢多线程同步与互斥有多重方法这里Jungle介绍互斥锁这种用法。代码如下

#ifndef __SINGLETON_H__
#define __SINGLETON_H__
 
#include <iostream>
#include <string.h>
#include <mutex>
using namespace std;
 
class Singleton
{
public:
	static Singleton* getInstance(){
		if (instance == NULL){
			m_mutex.lock();
			if (instance == NULL){
				printf("创建新的实例\n");
				instance = new Singleton();
			}
			m_mutex.unlock();
		}
		return instance;
	}
private:
	Singleton(){}
 
	static Singleton* instance;
	static std::mutex m_mutex;
};
 
Singleton* Singleton::instance = NULL;
std::mutex Singleton::m_mutex;
 
#endif //__SINGLETON_H__

接下来再在多线程环境下测试,结果显示功能正常! avatar

4.单例模式总结

  • 优点:
    • 单例模式提供了严格的对唯一实例的创建和访问
    • 单例模式的实现可以节省系统资源
  • 缺点:
    • 如果某个实例负责多重职责但又必须实例唯一,那单例类的职责过多,这违背了单一职责原则
    • 多线程下需要考虑线程安全机制
    • 单例模式没有抽象层,不方便扩展
  • 适用环境:
    • 系统只需要一个实例对象
    • 某个实例只允许有一个访问接口