以下内容均来自GeekBand极客班C++ 设计模式课程(李建忠老师主讲)
Singleton
面向对象很好地解决了"抽象"的问题,但是必不可免地付出一定的代价,对于通常情况来讲,面向对象的成本大都可以忽略不计。但是某些情况,面向对象所带来的成本必须谨慎处理。
“对象性能”模式
典型模式:
Singleton
Flyweight
动机(Motivation)
在软件系统中,经常有这样一些特殊的类,必须保证它们在系统中存在一个实例,才能确保它们的逻辑正确性,以及良好的效率。
如果绕过常规的构造器,提供一个机制来保证一个类只有一个实例?
这应该是类设计者的责任,而不是使用者的责任。
示例
//singleton
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;
}
threadA和threadB必定阻塞,多线程安全,但是,影响性能
假设m_instance不是nullptr,多个线程进入,都是读操作而不是写操作,读操作是可以并发的(极高并发,如web服务器,很浪费)
//双检查锁 锁前锁后都检查 double check
Singleton * Singleton::getInstance(){
if(m_instance == nullptr){
Lock lock;//函数结束后直接释放
if(m_instance == nullptr){
//一定要再判读一次,一面thread在进入第一个if后,再次出现被两个线程执行两次
m_instance = new Singleton();
}
}
//return 读的部分可以并发
return m_instance;
}
但是以上内容依旧存在漏洞,内存读写会出现reorder的情况
m_instance = new Singleton();
//理论部分:
//先分配内存
//调用构造器
//内存地址给m_instance
//实际部分(有可能):
//先分配内存
//内存地址给m_instance
//调用构造器
当threadA执行到了实际的第二部,threadB进行,执行到了if判断,显然不为nullptr,毕竟已经把内存地址给了
然后threadB去使用这个m_instance,发生错误,因为threadA中并没有调用构造器
//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);//获取内存
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(std::memory_order_relaxed);
}
}
//return 读的部分可以并发
return tmp;
}
要点总结
Singleton模式中的实例构造器可以设置为protected以允许子类派生。
Singleton模式一般不要支持拷贝构造函数和Clone接口,因为这有可能导致多个对象实例