# 单例模式
单例模式是设计模式的一种,是开发实践中经常用到的一个概念。大意是在一个生命周期内(大多数情况是一个进程),只有一个实例。比如一个进程应该只有一个日志管理器 (LogManager) 实例。
这里给出一个单例模式的例子:
class Singleton { | |
static Singleton *instance; | |
int data; | |
// Private constructor so that no objects can be created. | |
Singleton() { | |
data = 0; | |
} | |
public: | |
static Singleton *getInstance() { | |
if (!instance) | |
instance = new Singleton; | |
return instance; | |
} | |
int getData() { | |
return this -> data; | |
} | |
void setData(int data) { | |
this -> data = data; | |
} | |
}; | |
//Initialize pointer to zero so that it can be initialized in first call to getInstance | |
Singleton *Singleton::instance = 0; | |
int main(){ | |
Singleton *s = s->getInstance(); | |
cout << s->getData() << endl; | |
s->setData(100); | |
cout << s->getData() << endl; | |
return 0; | |
} |
其中 Singleton 类不能从外部实例化,只能调用 getInstance 静态方法生成 Singleton 的实例。
# 单例模式进阶
上面的例子可以精简一下:
class Singleton { | |
static Singleton *instance; | |
int data; | |
Singleton() { | |
data = 0; | |
} | |
//... | |
public: | |
static Singleton *getInstance(); | |
} |
这里的 instance 静态成员变量不会被释放,导致可能存在泄漏。这里的泄漏大多是文件泄漏等,因为可能有未保存的文件内容等。
所以这里的 instance 可以使用智能指针:
#include <memory> | |
class Singleton { | |
static std::unique_ptr<Singleton> instance; | |
int data; | |
private: | |
Singleton() { | |
data = 0; | |
} | |
//... | |
public: | |
static Singleton* getInstance(); | |
} | |
// initialization of 'instance' variable | |
// ... | |
Singleton* Singleton::getInstance(){ | |
if (!instance) | |
instance = std::unique_ptr<Singleton>(new Singleton); // you can use std::make_unique | |
return instance.get(); | |
} |
由于这里能保证单例是肯定可以分配成功的,因为 new 大多数情况下不会失败,所以 getInstance 的返回类型可以改为:
static Singleton& getInstance(); |
然后,这个版本不是多线程安全的,因此需要加锁。但比较便捷的方式是如下:
#include <memory> | |
class Singleton { | |
int data; | |
public: | |
Singleton(const Singleton&) = delete; | |
Singleton& operator= (const Singleton) = delete; | |
private: | |
Singleton() { | |
data = 0; | |
} | |
//... | |
public: | |
static Singleton& getInstance(); | |
} | |
Singleton& Singleton::getInstance(){ | |
static Singleton instance; | |
return instance; | |
} |
在 c++11 中,静态局部变量的初始化是多线程安全的。
这里禁止了复制构造函数,因为单例类只需要一个实例。
# 单例模式模板进阶
每个类都写一个 getInstance 是比较麻烦的,我们可以使用模板来更方便的完成单例模式:
template<typename T> | |
class Singleton { | |
public: | |
static T& instance(); | |
Singleton(const Singleton&) = delete; | |
Singleton& operator= (const Singleton) = delete; | |
protected: | |
struct token {}; //without needing to be a friend. | |
Singleton() {} | |
}; | |
#include <memory> | |
template<typename T> | |
T& Singleton<T>::instance() | |
{ | |
static T instance{token{}}; | |
return instance; | |
} |
模板的详情可参考这个。
# 单例模式引入的问题(针对传统的使用方式)
单例模式可以带来很多好处,因为我们不需要频繁的创建单例类的实例,在需要单例类的实例时,调用 getInstance 获取即可。
然而,单例模式也会带来一些问题,比如这个讨论和这篇文章。
如果空泛的解释这个问题,似乎有点没有头绪,那么接下来描述几个例子。
# Singletons are Pathological Liars
eg:Singletons are Pathological Liars
# 单例类之间的依赖引发崩溃
auto a = SingletonA::Intance(); | |
a.func1(); | |
// func1 implementation | |
SingletonA::func1(){ | |
//... some work | |
single_b = SingletonB::Intance(); | |
single_b.func2(); | |
} |
如果 SingletonA 和 SingletonB 的 Instance 都是第一次被调用,那么 SingletonA 先被构造,之后是 SingletonB,在进程退出时,SingletonB 会先被析构,如果此时进程的某个线程调用了 a.func1,那么由于 SingletonB 的实例被析构了,所以可能出现访问异常,导致崩溃。
# 静态全局变量和单例类之间的依赖引发崩溃
auto a = SingletonA::Intance(); | |
a.func1(); | |
// func1 implementation | |
SingletonA::func1(){ | |
//... some work | |
static classB b; | |
b.func2(); | |
} |
该崩溃情况和上一小节的原理是一致的。
# 上述单例类的实现不是多线程安全的
在进程退出时,单例类的实例会被析构,如果此时有一个线程在使用这个单例类的实例,那么也可能会引发崩溃,但这种的情况概率一般比较低。
# 这些问题的原因
以上问题有三个点:
依赖关系隐藏,导致新接手工程的开发者不清楚依赖关系。
单例类在上述的实现方式中,是使用时初始化 (调用构造函数),那么多个单例类之间的初始化顺序是不可预测的,这会导致彼此的依赖不确定性。
单例类在某种意义上,可以理解成是一个全局变量,全局变量不是多线程安全的,因此单例类也同样如此。
# 解决办法
- 在进程的
main
方法中,显示的定义多个单例类之间的初始化顺序,保证在进程退出后,这些单例类的实例按照初始化的逆顺序进行析构。 - 在需要的场景下,对单例类的析构函数和其他函数加锁。
之前想过在 main 方法的最后显示调用单例模式的析构函数,发现代码不知道怎么写。换一个角度思考,显示调用析构函数从设计上是不好的,另外,既然运行时做了单例类实例的初始化,那么相应的析构也应该由运行时来做。因此在 main 方法中调用析构函数是不合理的。
# 使用单例模式的原因
既然单例模式有上述问题,那为什么还要使用单例模式呢?
回到单例模式的初衷:保证一种资源在一个特定的环境下只有一个实例。
# 理想的解决办法
回顾单例模式的问题,可发现单例模式本身是没有问题的,错在单例模式的使用方法上。
那么如何更好的解决以上问题,以下是一个可行的办法:
使用单例模式创建实例(保证唯一实例)
给每个需要单例类实例的组件传一个单例类实例的参数(暴露依赖)
- 这里和全局使用单例类实例的区别在于依赖被显式声明,同时依赖的顺序将会被固定,不存在不确定性。
- 单元测试中,多线程情况下需要等待任务执行结束 (wait method),再退出单元测试。
在以上情况都不适用的情况下,可使用传统使用方式。
# 总结
单例模式有它的使用场景,其概念是好的,没有问题的。之所以被大家诟病的原因,其实是部分开发者对于单例模式的不当使用(因为全局状态 (global state) 的传递变得没有条理)。
另外,单例模式的使用中,需要注意单例类之间的初始化顺序,当然也要尽量避免单例类之间的依赖(不过这往往不太好避免)。
单例模式不易使用的场景是包含重要状态的一些类,这些类最好使用依赖传递。因为使用单例模式的话,会隐藏依赖关系,这样对于状态的改变就难以跟踪和分析。这时使用上一节提出的方法就会更好一点。
# 参考链接
- Alternatives to the singleton pattern
- What are drawbacks or disadvantages of singleton pattern?
- Root Cause of Singletons
- Modern C++ Singleton Template
- Singleton Destructors
- Patterns I Hate #1: Singleton