一 前言
单例模式是面向对象设计模式中的一种,什么是单例模式,单例模式就是指某个类只可以实例化出一个对象,不可以实例化出多个对象,在工程中单例模式有比较多的应用,例如http服务器类,对于某网络服务来说http服务器一般只有一个,所以http服务器对象一般只有一个,再比如创建和销毁比较平凡的工具类,为了减少频繁的创建和销毁对象造成计算机效率低下,也通常使用单例模式;还有线程池、打印机这些只需要一个对象的类。
知道了单例模式的定义,那么我们来思考如何设计单例模式呢?
1. 单例模式只可以创建一个对象,那么构造函数必须是private类型的,否则外界将可以直接多次调用构造函数实例化多个对象,此外复制构造化函数没有存在的意义,只有一个对象,何来复制一说!
2. 第二,我们需要一个标志符来表示当前类是否实例化出了对象,若已经实例化对象则不可以再实例化对象,否则可以实例化。
3. 第三,析构函数也是私有的,一般单例模式实例化出的对象不会消亡,若消亡也需要和构造函数一样,在public中用相应的函数实现。
4. 似乎目前只要考虑这些就行,我们先实现代码,看着这样行不行,若存在问题,再进行补充。
二 代码实现及改进
1. 初版代码
class Singleton {
public :
static Singleton* getInstance() {
if (instance == nullptr) {
instance = new Singleton();
}
return instance;
}
private :
static Singleton* instance;
Singleton() {};
Singleton(Singleton &obj) = delete;
~Singleton() {}
};
Singleton *Singleton::instance = nullptr;
int main() {
Singleton *p1 = Singleton::getInstance();
Singleton *p2 = Singleton::getInstance();
cout << "p1: " << p1 << ", p2: " << p2 << endl;
return 0;
}
这里插一句:类属性在类中只是声明,需要程序员在类外进行定义。
代码运行结果如下:
2. 多线程带来的影响
上面的代码从输出来看,似乎是没有问题的,但是却忽略了一点,那就是程序在多线程环境中运行存在错误,假设现在有A, B两个线程同时执行到第4行 if (instance == nullptr)
判断,两个线程if判断均为真,都会实例化对象,这显然不行。那如何解决这个问题呢?答案就是:互斥锁(锁和信号量在多线程编程中扮演着重要的角色,需要牢记)。
所以我们在类中加上互斥锁,防止此类问题的发生,代码如下:
class Singleton {
public :
static Singleton* getInstance() {
std::unique_lock<std::mutex> lock(m_mutex);
if (instance == nullptr) {
instance = new Singleton();
}
return instance;
}
private :
static std::mutex m_mutex;
static Singleton* instance;
Singleton() {};
Singleton(Singleton &obj) = delete;
~Singleton() {}
};
Singleton *Singleton::instance = nullptr;
std::mutex Singleton::m_mutex;
我们在函数开始加上锁,这样多线程环境中只有拿到互斥锁的线程才可以继续向下执行。有些读者可能会说这里只有加锁,没有解锁,其实对于unique_lock<>() 而言,出了其作用范围之后,就会自动解锁,故无需担心。
但是这样就很好了吗?非也!
假设现在已经实例化出了一个对象,下一次再次调用getInstance()
函数实例化对象,首先要获取互斥锁,之后if判断发现对象已经实例化,此时直接返回已经实例化的对象的指针,并且解锁。要知道获取互斥锁和解锁,都会花费一定的时间,是有代价的,所以我们是不是在加锁之前,先判断对象是否已经实例化,若已实例化,则直接返回对象地址,从而省略每次的加锁操作,提高效率。代码如下:
class Singleton {
public :
static Singleton* getInstance() {
if (instance == nullptr) {
std::unique_lock<std::mutex> lock(m_mutex);
if (instance == nullptr) {
instance = new Singleton();
}
}
return instance;
}
private :
static std::mutex m_mutex;
static Singleton* instance;
Singleton() {};
Singleton(Singleton &obj) = delete;
~Singleton() {}
};
Singleton *Singleton::instance = nullptr;
std::mutex Singleton::m_mutex;
这样代码就完成了。但是我们思考一下,有了外层的if (instance == nullptr)
里层的if (instance == nullptr)
是不是就有些多余呢?其实不是的,这还是从多线程环境中举反例,若去除内层判断,假设线程A, B都通过了外层if判断,线程A拿到锁,执行完后,释放锁,随后线程B拿到锁,没有了内层if判断,将会再实例化对象,这显然是不行的。
三 饿汉模式和懒汉模式
单例模式分为饿汉模式和懒汉模式两种,上面说的是懒汉模式(什么时候调用getInstance()函数才什么时候实例化对象,是不是有些懒?),而饿汉模式则不同,直接程序开始运行的时候就直接实例化对象,代码如下:
class Singleton {
public :
static Singleton* getInstance() {
return instance;
}
private :
static std::mutex m_mutex;
static Singleton* instance;
Singleton() {};
Singleton(Singleton &obj) = delete;
~Singleton() {}
};
Singleton *Singleton::instance = new Singleton();
std::mutex Singleton::m_mutex;
等一下,不是把构造函数私有化了吗?为什么还可以直接new Singleton()
,这有问题呀!!!
类中将属性和方法分为public, protected, private三类,表示的是类外对类内部成员的可见程度,注意是类外看类内,但是这里的instance本就是类内元素,所以自然可以调用类内的构造函数,自然也就可以new。
单例模式总的来说:分为懒汉模式和饿汉模式,懒汉模式更加优美,需要注意加锁和两层if
判断,以及相关变量和函数的私有化。