问题场景
在日常开发中,数据库是绕不开的一块内容。为了完成数据库的连接并进行查询的操作,我们在配置文件中保存了数据库的连接信息、用户名和密码。接下来尝试连接上服务器并进行查询操作。
简单粗暴的方法
解决思路
一种最简单直接的方法是,直接创建一个类。类中包含了数据库的连接信息、用户名和密码。每一次查询前,都从配置文件中读取这些内容,再连接数据库,并执行查询操作。
对应代码
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();
这行语句时,一共需要做三件事。
- 分配内存;
- 调用构造函数;
- 赋值给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;
}