“对象性能”模式
面向对象很好的解决了“抽象”的问题,但是必不可免地要付出一定的代价。对于通常情况来讲,面向对象的成本大都可以忽略不计。但是某些情况,面向对象所带来的成本必须谨慎处理。
典型模式
- Sington
- Flyweight
单例模式Singleton
保证一个类仅有一个实例,并提供一个该实例的全局访问点。——《设计模式》GoF
动机
在软件系统中,经常有这样一个特殊的类,必须保证它们在系统中只存在一个实例,才能确保他们的逻辑正确性、以及良好的效率。如何绕过常规构造器,提供一种机制来保证一个类只有一个实例,这个应该类设计者的责任,而不是使用者的责任。
模式选择
Singleton模式典型的结构图为:
在Singleton模式的结构图中可以看到,我们通过维护一个static的成员变量来记录这个唯一的对象实例。通过提供一个staitc的接口instance来获得这个唯一的实例。
局部静态变量
这种方式很常见,实现非常简单,而且无需担心单例的销毁问题。
// singleton.h
#ifndef SINGLETON_H
#define SINGLETON_H
#include <iostream>
using namespace std;
// 单例
class Singleton
{
public:
static Singleton& getInstance()
{
static Singleton instance;
return instance;
}
private:
Singleton() {} // 构造函数(被保护)
Singleton(Singleton const &); // 无需实现
Singleton& operator = (const Singleton &); // 无需实现
};
#endif // SINGLETON_H
这样以来,既可以保证只存在一个实例,通过getInstance可以访问,又不用考虑内存回收的问题。
懒汉式/饿汉式
懒汉式 | 饿汉式 |
Lazy 初始化,非多线程安全 | 非 Lazy 初始化,多线程安全 |
优点:第一次调用才初始化,避免内存浪费。 |
优点:没有加锁,执行效率会提高。
|
缺点:必须加锁(在“线程安全”部分分享如何加锁)才能保证单例,但加锁会影响效率。 | 缺点:类加载时就初始化,浪费内存。 |
class Singleton{
public:
static Singleton* getInstance();
static Singleton* m_instance;
private:
singleton();
Singleton(const Singleton& other);
Singleton& operator = (const Singleton &);
}
//懒汉
Singleton* Singleton::m_instance=nullptr ;
//线程非安全版
Singleton* Singleton::getInstance()
{
if(m_instance==nullptr)
{
m_instance=new Singleton();
}
return m_instance;
}
//线程安全版本 ,但是锁的代价过高
Singleton* Singleton::getInstance()
{
Lock lock; //全局加锁,直到return
if(m_instance==nullptr)
{
m_instance=new Singleton();
}
return m_instance;
}
//***********************************************/
//饿汉
Singleton* Singleton::m_instance=new Singleton() ;
//
Singleton* Singleton::getInstance()
{
return m_instance;
}
双检查锁
懒汉模式的全局加锁代价太高,因为只读的情况下是不需要加锁。特别是在高并发情况下代价很高
Singleton* Singleton::m_instance=nullptr ;
Singleton* Singleton::getInstance()
{
if(m_instance==nullptr)
{
Lock lock;
if(m_instance==nullptr)
m_instance=new Singleton();
}
return m_instance;
}
双检查锁的问题是由于内存读写reorder不安全,正常new执行 是分配内存-》调用构造器进行初始化-》返回内存地址,编译器优化,reorder之后 分配内存-》返回内存地址-》调用构造器进行初始化,如果A线程返回了没有调用构造器的地址,且线程B直接使用就会出现问题
//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;
}
资源释放
有内存申请,就要有对应的释放,可以采用下述两种方式:
- 主动释放(手动调用接口来释放资源)
- 自动释放(由程序自己释放)
要手动释放资源,添加一个 static 接口,编写需要释放资源的代码:
static void DestoryInstance()
{
if (m_instance != nullptr) {
delete m_instance;
m_instance= nullptr;
}
}
然后在需要释放的时候,手动调用该接口
Singleton::getInstance()->DestoryInstance();
当程序较复杂市在哪里释放不好确定,应该让其自动释放。解决方法就是定义一个嵌套类
//垃圾回收
class Singleton
{
public:
static Singleton* getInstance();
~Singleton()
{
cout<<"~Singleton ..."<<endl;
}
class Garbo
{
public:
~Garbo()
{
if (Singleton::m_instacne_ != NULL)
{
delete m_instacne_;
m_instance=nullptr;
}
}
};
private:
Singleton(const Singleton& other);
Singleton& operator=(const Singleton& other);
Singleton()
{
cout<<"Singleton ..."<<endl;
}
static Singleton* m_instacne;
static Garbo garbo_; // 利用对象的确定性析构
};
Singleton::Garbo Singleton::garbo_;
Singleton* Singleton::m_instacne;
在程序运行结束时,系统会调用 Singleton 的静态成员 GC 的析构函数,该析构函数会进行资源的释放。
Singleton的要点总结
- Singleton模式中的实力构造器可以设置为protected,以允许子类派生
- Singleton模式一般不要支持拷贝构造函数和Clone接口,因为有可能会导致多个对象实例,与Singleton模式的初衷相违背。
- 如何实现多线程环境下安全的Singleton?注意对双检查锁的正确实现。