1.为什么使用模板化单例类:
如果你需要为不同类型的对象实现单例,而没有使用模板化设计,那么你可能需要为每个类型都实现一个单例类,这会导致大量重复代码。模板化的单例类通过代码复用来减少这种冗余。将单例类设置为模板的主要目的是为了实现不同类型的单例,而无需为每个类型编写单独的单例类。模板提供了一种灵活且通用的方式来实现单例模式,使其能够适应不同类型的需求。在模板化单例中,每个类型会有自己的静态 _instance
成员变量,确保每个类型都有自己独立的单例实例,不会相互干扰。而且使用模板确保了在编译时就确定了单例的类型,不会出现类型不匹配的问题。这样可以保证类型安全,避免运行时类型错误。
2.模板化单例类代码实现:
template <typename T>
class Singleton
{
protected:
Singleton()=default; //希望基类继承的时候可以构造,所以设置为保护
Singleton(const Singleton<T>&)=delete;
Singleton& operator=(const Singleton<T>&)=delete;
static std::shared_ptr<T> _instance;
public:
static std::shared_ptr<T> GetInstance(){
static std::once_flag s_flag;
std::call_once(s_flag,[&](){
_instance=std::shared_ptr<T>(new T);
});
return _instance;
}
void PrintAddress(){
std::cout<<_instance.get()<<std::endl;
}
~Singleton(){
std::cout<<"this is Singleton destruct!"<<std::endl;
}
};
template <typename T>
std::shared_ptr<T> Singleton<T>::_instance=nullptr;
3.三种单例类实现方式对比:
我们通过一个静态智能指针管理一个T类型的对象,可以用来实例出不同类型的单例,并且只有一个。同时,我们使用:
std::once_flag
是一个标志,用来确保 std::call_once
内的代码块只会被执行一次。
std::call_once
接受一个标志s_flag
和一个要执行的代码块。在多线程环境中,std::call_once
确保只有一个线程会执行这个代码块,其他线程会阻塞,直到代码块执行完毕。- 线程安全:
std::call_once
能确保即使在多线程环境下,_instance
只会被初始化一次。 - 高效性:一旦
std::call_once
完成初始化,之后的所有调用都会直接返回,不再涉及锁机制。
相比以前的空指针判断 + 双重检查锁定(懒汉模式):
if (_instance == nullptr) {
std::lock_guard<std::mutex> lock(s_mutex);
if (_instance == nullptr) {
_instance = std::shared_ptr<T>(new T);
}
}
这种方式易于理解,但是存在性能开销:即使 _instance
已经初始化完毕,每次检查时仍然需要锁定和解锁,这在高并发环境下可能带来性能开销。另外,由于new底层分配内存的操作(先分配内存,然后调用构造函数构造对象),在多线程环境下,可能会遇到这样的情况:线程A正在执行 new
操作,但在对象构造完成之前,线程B已经检测到 _instance
指针不为空。这会导致线程B访问未完全构造的对象,从而引发未定义行为。
还有个懒汉模式的变种,通过维护一个静态对象 ,并且在第一次使用时初始化:
static Singleton& GetInstance() {
static Singleton instance; // 第一次调用时创建实例
return instance;
}
这种方式也是既安全又节约资源的,但是静态局部变量的生命周期直到程序结束,因此可能导致实例的销毁时间较长,尽管如此,与双重检查锁定加 std::once_flag
相比,静态局部变量法没有额外的性能开销,它只在第一次调用时进行初始化,并且之后不会再进行额外的检查,是更推荐的选择。
最后,说说饿汉模式:
static Singleton instance; // 在类加载时初始化
static Singleton& getInstance() {
return instance;
}
Singleton Singleton::instance; // 类加载时创建实例
优点是简单,且线程安全。但缺点也很明显:类静态成员变量在程序启动时就会被初始化,即使你从未调用 getInstance()
方法,实例也已经存在。无论是否使用这个单例对象,它都会在程序启动时就创建,占用资源。