【C++设计模式】单例模式

323 阅读7分钟

问题场景

在日常开发中,数据库是绕不开的一块内容。为了完成数据库的连接并进行查询的操作,我们在配置文件中保存了数据库的连接信息、用户名和密码。接下来尝试连接上服务器并进行查询操作。

简单粗暴的方法

解决思路

一种最简单直接的方法是,直接创建一个类。类中包含了数据库的连接信息、用户名和密码。每一次查询前,都从配置文件中读取这些内容,再连接数据库,并执行查询操作。

对应代码

class Singleton
{
private:
	std::string m_connect;
	std::string m_username;
	std::string m_password;
public:
	Singleton(const std::string& connect, const std::string& username, const std::string& password);
	bool query();
};

Singleton::Singleton(const std::string& connect, const std::string& username, const std::string& password) 
	:m_connect(connect), m_username(username), m_password(password) {
	std::cout << "###构造实例###" << std::endl;
	std::cout << "连接:" << m_connect << std::endl;
	std::cout << "用户:" << m_username << std::endl;
	std::cout << "密码:" << m_password << std::endl;
	std::cout << "###构造完毕###" << std::endl;
}

bool Singleton::query() {
	std::cout << "进行了一次数据库查询操作" << std::endl;
	return true;
}

待改进点

数据库往往需要多次访问。这种思路的问题是,每一次访问都会导致对配置文件的又一次读取,从而导致数据库的访问速度大幅下降。但数据库的连接信息、用户名和密码是不会随意发生变化的,我们仅需要读取一次数据库的配置文件,并将对应信息保存到内存中。访问数据库时直接从内存中拿数据,就能够避免重复读取带来的时延。

懒汉式单例

解决思路

在上面的代码中,我们每一次创建类都需要先定义三个临时变量,分别对应数据库的连接信息、用户名和密码。而临时变量在超出自身作用域后,会被销毁。因此,我们自然会想到采用static来对变量进行修饰,从而避免多次创建和销毁临时变量。更进一步考虑,三个变量处理起来还是有些麻烦,为了后续使用更加方便,干脆就用一个类来封装一下,来对这三个变量进行统一的管理。

同时,为了避免该类被重复创建,我们隐藏了类的构造函数,转而使用一个getInstance()方法,该方法创建一个静态指针指向类的实例。如果该指针不为空,则说明实例已经存在,直接返回实例,无需二次创建。

对应代码

class Singleton
{
private:
	std::string m_connect;
	std::string m_username;
	std::string m_password;
	Singleton();
public:
	static Singleton* config;
	static Singleton* getInstance();
	bool query();
};

Singleton* Singleton::config = nullptr;

Singleton* Singleton::getInstance() {
	if (config == nullptr) {
		config = new Singleton();
	}
	return config;
}

Singleton::Singleton() {
	std::cout << "正在读取配置文件" << std::endl;
	std::fstream fs("./SingletonConfig.txt");
	char tempStr[1024];
	int line_index = 0;
	while (fs.getline(tempStr, 1024)) {
		switch (line_index++)
		{
		case 0:
			m_connect = tempStr;
			break;
		case 1:
			m_username = tempStr;
			break;
		case 2:
			m_password = tempStr;
			break;
		default:
			break;
		}
	}
	std::cout << "###构造实例###" << std::endl;
	std::cout << "连接:" << m_connect << std::endl;
	std::cout << "用户:" << m_username << std::endl;
	std::cout << "密码:" << m_password << std::endl;
	std::cout << "###构造完毕###" << std::endl;
}

bool Singleton::query() {
	std::cout << "进行了一次数据库查询操作" << std::endl;
	return true;
}

待改进点

程序启动之后,当类被第一次创建时,需要读取配置文件。这就导致程序在第一次执行时,速度较慢

饿汉式单例

解决思路

配置文件是必须被读取的。为了避免当程序在第一次执行时,读取配置文件造成的时延,我们可以采用预加载的方式,在程序开始执行前,解决这个问题。

同时,为了增加程序的健壮性,将config指针设置为private,避免直接被外部调用。同时,为了避免单例类的唯一实例被拷贝,我们将拷贝构造函数也设置为private

提到拷贝构造,自然也会想到类似的赋值运算符重载。考虑以下情况:

Singleton* x = Singleton::getInstance();
Singleton* y = Singleton::getInstance();
*x = *y;

为了避免这种无意义的操作可能导致的错误,我们对赋值运算符重载进行处理,保证其只返回指向唯一实例的this指针,并将其私有化。

对应代码

class Singleton
{
private:
	std::string m_connect;
	std::string m_username;
	std::string m_password;
	Singleton();
        static Singleton* config;
        Singleton(const Singleton& config);
        Singleton& operator= (const Singleton& config);
public:
	static Singleton* getInstance();
	bool query();
};

Singleton* Singleton::config = new Singleton();

Singleton* Singleton::getInstance() {
	return config;
}

Singleton::Singleton() {
	std::cout << "正在读取配置文件" << std::endl;
	std::fstream fs("./SingletonConfig.txt");
	char tempStr[1024];
	int line_index = 0;
	while (fs.getline(tempStr, 1024)) {
		switch (line_index++)
		{
		case 0:
			m_connect = tempStr;
			break;
		case 1:
			m_username = tempStr;
			break;
		case 2:
			m_password = tempStr;
			break;
		default:
			break;
		}
	}
	std::cout << "###构造实例###" << std::endl;
	std::cout << "连接:" << m_connect << std::endl;
	std::cout << "用户:" << m_username << std::endl;
	std::cout << "密码:" << m_password << std::endl;
	std::cout << "###构造完毕###" << std::endl;
}

bool Singleton::query() {
	std::cout << "进行了一次数据库查询操作" << std::endl;
	return true;
}

Singleton& Singleton::operator=(const Singleton& config) {
	return *this;
}

待改进点

多线程场景下,使用懒汉模式时,类的实例可能会被重复创建,配置文件也可能会被重复读取,影响程序执行速度。在懒汉式单例中,我们有这样一段代码:

Singleton* Singleton::config = nullptr;

Singleton* Singleton::getInstance() {
	if (config == nullptr) {
		config = new Singleton();
	}
	return config;
}

在多线程场景下,假设存在线程A与线程B。线程A先启动,通过if条件之后,开始new操作。但由于new操作较为耗时,此时,线程B进行if判断时,线程A还没有new完。因此,线程B也会通过if的判断开始new操作。

多线程懒汉模式

解决思路

通过加锁,可以阻塞其它尝试加锁的线程,这样就可以保证在new的过程中,没有其它线程进行if的判断。这种方法当然可行,但有没有可以优化的地方?线程加锁导致的阻塞也会极大的影响程序的运行速度。在单例模式中,当第一个实例被new出来后,加锁的操作就不必要了。因此,我们可以在锁外再套一个if (config == nullptr),用来处理实例new后的情况。

程序到这里还没完,当程序执行到config = new Singleton();这行语句时,一共需要做三件事。

  1. 分配内存;
  2. 调用构造函数;
  3. 赋值给config指针;

这里的问题是,编译器在编译时,可能会对指令进行重排序(编译器会为了性能,在不改变依赖关系的前提下,调整指令的执行顺序)。这里的第2步与第3步都依赖于第1步,但第2步与第3步之间没有依赖关系。因此,第2步与第3步的实际执行顺序可能被调换。即,分配内存后,会先对config指针进行赋值,再调用构造函数。假设线程A按132的执行顺序执行到步骤3时,线程B开始进行if判断。此时由于config已经被赋值,不再为空,线程B会直接把config指针返回,但此时还未完成构造,B线程持有的单例是没有初始化完毕的实例。这里我们使用volatile关键字,要求编译器从内存中取得实际值,禁止编译器对指令进行重排序。

对应代码

class Singleton
{
private:
	std::string m_connect;
	std::string m_username;
	std::string m_password;
	Singleton();
	static Singleton* volatile config;
	Singleton(const Singleton& config);
	Singleton& operator= (const Singleton& config);
	static std::mutex mut;
public:
	static Singleton* volatile getInstance();
};

Singleton* volatile Singleton::config = nullptr;
std::mutex Singleton::mut;

Singleton* volatile Singleton::getInstance() {
	if (config == nullptr) {
		mut.lock();
		if (config == nullptr) {
			config = new Singleton();
		}
		mut.unlock();
	}
	return config;
}

Singleton::Singleton() {
	std::cout << "正在读取配置文件" << std::endl;
	std::fstream fs("./SingletonConfig.txt");
	char tempStr[1024];
	int line_index = 0;
	while (fs.getline(tempStr, 1024)) {
		switch (line_index++)
		{
		case 0:
			m_connect = tempStr;
			break;
		case 1:
			m_username = tempStr;
			break;
		case 2:
			m_password = tempStr;
			break;
		default:
			break;
		}
	}
	std::cout << "###构造实例###" << std::endl;
	std::cout << "连接:" << m_connect << std::endl;
	std::cout << "用户:" << m_username << std::endl;
	std::cout << "密码:" << m_password << std::endl;
	std::cout << "###构造完毕###" << std::endl;
}

Singleton& Singleton::operator=(const Singleton& config) {
	return *this;
}