概述( overview )
面向对象很好的解决了抽象的问题,但是不可避免的要付出一定的代价。对于通常情况来讲,面向对象的成本大都可以忽略不记。但是某些情况下面向对象带来的成本必须谨慎处理。
动机( Motivation )
在软件系统里面,经常一些特殊的类,必须保证它们所在的系统中只存在一个实例,才能确保他们的逻辑正确性,以及良好的效率。如何绕过常规的构造器,提供一种机制来保证一个类只有一个实例。这应该由设计者来完成而不是使用者来完成。
代码设计 (Code Design)
在设计的时候要将 构造函数和拷贝构造函数 不能设置成 Public 的(不设置的话对象生成会自动提供)。
class Singleton{
private:
Singleton();
Singleton(const Singleton& other);
public:
static Singleton* getInstance();
static Singleton* m_instance;
};
适用单线程的
Singleton* Singleton::m_instance=nullptr;
//线程非安全版本
Singleton* Singleton::getInstance() {
if (m_instance == nullptr) {
m_instance = new Singleton();
}
return m_instance;
}
适用多线程的
//线程安全版本,但锁的代价过高
Singleton* Singleton::getInstance() {
Lock lock;
if (m_instance == nullptr) {
m_instance = new Singleton();
}
return m_instance;
}
对于读事件其实并没有必要加锁,在高并发(比如十几万人同时访问的 web),锁的代价就出现了。可以采用双锁来改进 只有为空的时候才进行加锁。
解决高并发的方案
//双检查锁,但由于内存读写reorder不安全
Singleton* Singleton::getInstance() {
if(m_instance==nullptr){
Lock lock;
if (m_instance == nullptr) {
m_instance = new Singleton();
}
}
return m_instance;
}
虽然双锁的方法看似解决了问题,但是编译器的优化会欺骗代码,导致双锁失效。
理论上:执行 m_instance = new Singleton();(我们所期望的)要以下的几个步骤
- 分配内存
- 调用构造器,对内存进行初始化
- 把指向那块内存的指针返回给m_instance
但是编译器很可能会把 2,3 调换,这样就会出现问题了。
reorder之后,先分配内存,然后把内存分配给m_instance,此时m_instance就不是null了。一个线程进来发现m_instance不是null,就直接执行
return m_instance;
获取了对象实例,但是此时还没执行构造器,所以线程此时获取的是一块没被赋值的原生内存,这会导致错误发生。
新的解决方案(C++)
//C++ 11版本之后的跨平台实现 (volatile)
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);//获取内存fence
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);//释放内存fence
m_instance.store(tmp, std::memory_order_relaxed);
}
}
return tmp;
}
总结
- Singleton模式中的实例构造器可以设置为protected以允许子类派生。
- Singleton模式一般不要 支持拷贝构造函数和Clone接口, 因为这有可能导致多个对象实例,与Singleton模式的初衷违背。
- 如何实现多线程环境下安全的Singleton ?注意对双检查锁的正确实现。
写在最后的话
昨天是520,下面的话送给自己共勉
愿你身边有一个一直陪着你的人,ta 话不多,却让你感觉非常美好。