设计模式之单例模式(C++实现)
1. 什么是Singleton?
设计模式中的Singleton,中文翻译是单例模式(也有翻译单件模式),指的是限制一个类只能实例化出一个对象。这种看是奇特使用类的方式在某些情况下是有用的,比如整个系统中只有一个状态管理的类,又比如在Windows中的任务管理器。这些应用场景下只需要一个类的实例,多个实例反而不方便管理,也可能存在某些潜在的冲突,在这些应用中Singleton就可以很好地得到应用。
2. 替代方案
Singleton很明显可以使用一个全局的变量来替换(C++),但是相比于全局变量它有以下的优势:
可以避免引入与全局变量产生冲突
使用单例模式可以使用到延迟加载(lazy allocation and initialization),在没有使用的时候可以节约资源
3. C++中实现
单例模式的一种常用实现如下:
1)考虑到需要只有一个实例,因此常常使用一个静态的变量来表示;
2)为了避免类的使用者new出多个实例,需要将构造函数声明为private
3)但是需要有一种可以得到这个对象的方式,一般使用getInstatnce()这个public的方法来获取这个实例
综合上述介绍,一个简单的实现如下 1.
//定义
class Singleton {
public:
static Singleton* getInstance();
void doSomething();
protected:
Singleton();
private:
static Singleton* _instance;
};
//实现
Singleton* Singleton::_instance = nullptr;
Singleton* Singleton::getInstance() {
if (_instance == nullptr) {
_instance = new Singleton;
}
return _instance;
}
void Singleton::doSomething() {
std::cout << "Doing" << std::endl;
}
这个实现在单线程的环境下工作的很好,但是在多线程环境中可能存在着潜在的危险,考虑到两个线程同时运行到 if (_instance == nullptr),其中一个线程在这个时候被挂起,当另一个线程初始化_instance之后,唤醒挂起的线程,这时候该线程会继续创建一个实例,这与Singleton中单个实例的设计背道而驰了。
4. 解决方案
4.1 简单实现
既然上述的设计并非是线程安全的,那么我们在构造_instance的时候给它加锁不就好了吗?实现如下:
//定义
class Singleton {
public:
static Singleton* getInstance();
void doSomething();
protected:
Singleton();
private:
static Singleton* _instance;
};
//实现
Singleton* Singleton::_instance = nullptr;
std::mutex mutex;
Singleton* Singleton::getInstance() {
// 加上mutex使得每次只有一个线程进入
std::lock_guard<std::mutex> locker(mutex);
if (_instance == nullptr) {
_instance = new Singleton;
}
return _instance;
}
void Singleton::doSomething() {
std::cout << "Doing" << std::endl;
}
这样又会引入一个新的令人不爽的地方,线程在getInstance调用时都需要等待其他线程完成访问,这样不利于多线程发挥出它应有的优势。仔细看一下实现,发现实际上真正需要加锁的只有new这一个步骤,这样在初始化完成之后所有的线程就不会在进入到if这个分支中了,于是我们修改为:
Singleton* Singleton::getInstance() {
if (_instance == nullptr) {
// 加上mutex使得每次只有一个线程进入
std::lock_guard<std::mutex> locker(mutex);
_instance = new Singleton;
}
return _instance;
}
但是这样做有一个潜在的危险,当线程A和B都运行到if(_instance == nullptr)之后A线程挂起,B线程运行到加锁,new出对象,之后A线程被唤醒,它也会加锁并再次创建一个对象,于是又会出现两个实例,这与单例模式相悖。
4.2 DCLP方式
既然还是会创建新的对象,那么我们在创建之前再次判断一下不就好了吗,于是引出新的一个概念称为 The Double-Checked Locking Pattern(DCL
P),具体实现如下:
Singleton* Singleton::getInstance() {
if (_instance == nullptr) {
// 加上mutex使得每次只有一个线程进入
std::lock_guard<std::mutex> locker(mutex);
if (_instance == nullptr)
_instance = new Singleton;
}
return _instance;
}
接着上文的叙述,在A线程加锁之后发现B线程已经将_instance实例化了,于是_instance == nullptr为false,这样A就不会再次new出实例了,完美的解决了问题。
4.3 新的麻烦
使用DCLP就可以解决这个问题吗?事实上在某种程度上可以,但是不同的编译器在执行getInstance这个函数的时候有不同的处理方式,使得使用DCLP并不是一个适用于所有平台和编译器的完美解决方案,具体的细节十分复杂,读者可以参考下面这篇论文
C++ and the Perils of Double-Checked Locking2
文中给出了一个实现真正线程安全的一个实现(需要针对不同的操作系统实现),实现的模式是:
Singleton* Singleton::getInstance() {
Singleton* tmp = m_instance;
... // insert memory barrier
if (tmp == NULL) {
Lock lock;
tmp = m_instance;
if (tmp == NULL) {
tmp = new Singleton;
... // insert memory barrier
m_instance = tmp;
}
}
return tmp;
}
4.4 C++11的实现
C++11的一大亮点是引入了跨平台的线程库,通过线程库可以很好的完成上文中提到的问题。
4.4.1 实现方式一
这种实现方式实际上就是对上文中的DCLP的描述,实现代码如下:
std::atomic<Singleton*> Singleton::m_instance;
std::mutex Singleton::m_mutex;
Singleton* Singleton::getInstance() {
Singleton* tmp = m_instance.load(std::memory_order_relaxed);
std::atomic_thread_fence(std::memory_order_acquire);
if (tmp == nullptr) {
std::lock_guard<std::mutex> lock(m_mutex);
tmp = m_instance.load(std::memory_order_relaxed);
if (tmp == nullptr) {
tmp = new Singleton;
std::atomic_thread_fence(std::memory_order_release);
m_instance.store(tmp, std::memory_order_relaxed);
}
}
return tmp;
}
另外还有其他的实现方式,更多的内容可以参考:Double-Checked Locking Is Fixed In C++11 3
4.4.2 推荐的实现方式
对于singleton的实现方式,在Effective C++中作者实现了
class S
{
public:
static S& getInstance()
{
static S instance;
return instance;
}
private:
S() {};
S(S const&) = delete;
void operator=(S const&) = delete;
};
这种实现方式是线程安全的(C++11),在C++11的规范中有一下描述:
The C++11 standard §6.7.4:
If control enters the declaration concurrently while the variable is being initialized, the concurrent execution shall wait for completion of the initialization.
也就是说C++11保证了静态变量在被初始化的时候强制线程保持同步,这种方式就实现了无锁完美的方案,具体内部的实现是编译器的事情,它可能使用DCLP的方式或者其他的方式完成。但是C++11提到的这个语义保证了单例模式的实现。
更多的内容可以参考4