2 线程安全(双重校验,std::call_once,静态局部变量,静态全局变量)
3 内存释放(内嵌类垃圾回收,接口销毁,编译器自动回收,智能指针)
1 懒汉模式,饿汉模式
懒汉模式是指第一次用到该类实例的时候才会去实例化对象,这种模式下需要考虑线程安全,关于线程安全后面文章会介绍;饿汉模式指的是在单例类定义的时候就进行实例化,这种模式可以加快程序的启动速度,而且无需考虑线程安全。所以在访问量比较大,或者可能访问的线程比较多时,采用饿汉实现,可以实现更好的性能,以空间换时间;在访问量较小时,采用懒汉实现,以时间换空间。
2 线程安全(双重校验,std::call_once,静态局部变量,静态全局变量)
线程安全即是指保证在多线程访问的情况下,也能保证只实例化一个对象,且不会因为线程间的同步问题导致某个线程获取到的实例还没有分配好内存。我们先来看看使用静态局部变量的方法。
class Singleton
{
public:
static Singleton& Instance()
{
static Singleton singleton; //静态局部变量,第一次调用时才实例化
return singleton;
}
private:
Singleton() { };
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
};
静态局部变量在C++11以后是线程安全的,而且只有在第一次使用到才实例化,提高启动性能,但是程序初始化及关闭时单例的构造及析构顺序的不确定也可能导致致命的错误。接下来我们看看全局静态变量的实现。
class Singleton
{
public:
static Singleton& Instance()
{
return singleton;
}
private:
Singleton() { };
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static Singleton singleton;
};
Singleton Singleton::singleton;
全局静态变量不需要考虑多线程安全,虽然这种实现在一定程度下能良好工作,但是在某些情况下会带来问题, 就是在C++中 ”非局部静态对象“ 的 ”初始化“ 顺序 的 ”不确定性“, 例如: 如果有两个这样的单例类,将分别生成单例对象A, 单例对象B. 它们分别定义在不同的编译单元(cpp中), 而A的初始化依赖于B 【 即A的构造函数中要调用B::Instance() ,而此时B::singleton 可能还未初始化,显然调用结果就是非法的 】, 所以说只有B在A之前完成初始化程序才能正确运行,而这种跨编译单元的初始化顺序编译器是无法保证的。下面我们看看双重校验线程安全的实现。
class SingleInstance
{
public:
static SingleInstance *getInstance()
{
if(m_pInstance == NULL)
{
std::unique_lock<std::mutex> lock(m_Mutex);
if(m_pInstance == NULL)
{
m_pInstance = new SingleInstance ();
}
}
return m_pInstance;
}
static void deleteInstance()
{
std::unique_lock<std::mutex> lock(m_Mutex); // 加锁
if (m_pInstance )
{
delete m_pInstance ;
m_pInstance = nullptr;
}
}
private:
explicit SingleInstance ();
SingleInstance (const SingleInstance &) = delete;
SingleInstance & operator=(const SingleInstance &) = delete;
static SingleInstance *m_pInstance;//定义单例指针
static std::mutex m_Mutex;
};
SingleInstance *SingleInstance ::m_SingleInstance = nullptr;
std::mutex SingleInstance ::m_Mutex;
这种实现方式下,即使懒汉模式下也是线程安全的,通过一个对外的接口释放掉实例化的静态指针,需要用户自己调用,比较麻烦。下面看看用C++11新特性的std::call_once实现的方式,它是线程安全的。
#include <iostream>
#include <memory>
#include <mutex>
class Singleton {
public:
static std::shared_ptr<Singleton> getSingleton();
~Singleton() {
std::cout << __PRETTY_FUNCTION__ << std::endl;
}
private:
Singleton() {
std::cout << __PRETTY_FUNCTION__ << std::endl;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
};
static std::shared_ptr<Singleton> singleton = nullptr;
static std::once_flag singletonFlag;
std::shared_ptr<Singleton> Singleton::getSingleton() {
std::call_once(singletonFlag, [&] {
singleton = std::shared_ptr<Singleton>(new Singleton());
});
return singleton;
}
3 内存释放(内嵌类垃圾回收,接口销毁,编译器自动回收,智能指针)
关于实例化后单例的内存释放也是需要考虑的,对于静态局部变量和全局静态变量的实现方式,它们的资源回收都是交给编译器自动回收的,这样虽然方便,但是就如前面所言,对于全局静态变量它的初始化顺序是不确定的,有可能造成其他问题。对于接口销毁如上一小节的双重校验实现的例子,提供deleteInstance()的public接口给外部调用,这要求使用者了解这样的实现方式,但是如果忘记调用,在错误的地方调用或者多次调用,都可能造成麻烦,所以可以采用智能指针的方式,将自动释放内存,实现如上一小节std::call_once例子。也可以使用内嵌类垃圾回收,如下。
class SingleInstance
{
public:
static SingleInstance &getInstance()
{
if(m_pInstance == nullptr)
{
std::unique_lock<std::mutex> lock(m_Mutex);
if(m_pInstance == nullptr)
{
m_pInstance = new SingleInstance ();
}
}
return *m_pInstance;
}
private:
explicit SingleInstance ();
SingleInstance(const SingleInstance &) = delete;
SingleInstance& operator=(const SingleInstance &) = delete;
static SingleInstance *m_pInstance;//定义单例指针
static std::mutex m_Mutex;
class Garbo //专门用来析构m_pInstance指针的类
{
public:
~Garbo()
{
if(m_pInstance != nullptr)
{
delete m_pInstance;
m_pInstance = nullptr;
}
}
};
static Garbo m_garbo;
};
SingleInstance *SingleInstance ::m_SingleInstance = nullptr;
std::mutex SingleInstance ::m_Mutex;
Garbo SingleInstance ::m_garbo;
内嵌类垃圾回收通过静态变量析构时,delete单例指针从而释放了内存。注意以上的例子为了防止外部new或者拷贝构造和赋值构造,将构造设置为private接口,同时删除掉赋值操作符和拷贝构造。
4 返回引用和指针的区别
单例模式返回的实例的生存期是应该由单例本身所决定的,而不是用户代码。而指针和引用在语法上的最大区别就是指针可以为NULL,并可以通过delete运算符删除指针所指的实例,而引用则不可以。由该语法区别引申出的语义区别之一就是这些实例的生存期意义:通过引用所返回的实例,生存期由非用户代码管理,而通过指针返回的实例,其可能在某个时间点没有被创建,或是可以被删除的。所以如果返回对象指针,外部可能会误操作将其delete,这就会造成多次释放空间的问题。
5 可重用(模板)
如果我们的工程有很多的类需要设计成单例模式,那么按照上面的写法我们每一个类都得去实现,就显得有点写重复的代码了。C++的模板编程可以很好解决这个问题,下面分享一个网上看到的通过可变参类模板实现的单例模式,之前看到一个网页浏览器开源代码上也用到了这种实现方式。
#ifndef SINGLETON_H
#define SINGLETON_H
#include <mutex>
template <class T>
class Singleton
{
private:
Singleton() = default;
~Singleton() = default;
Singleton(const Singleton&)=delete;
Singleton& operator=(const Singleton&)=delete;
public:
template <typename... Args>
static T* instance(Args&&... args)
{
std::call_once(_flag, [&](){
_instance = new T(std::forward<Args>(args)...);
});
return _instance;
}
class Grabo
{
public:
~Grabo()
{
if (_instance)
{
delete _instance;
_instance = NULL;
}
}
};
private:
static T* _instance;
static std::once_flag _flag;
static Grabo _grabo;
};
template <class T>
T* Singleton<T>::_instance = NULL;
template <class T>
std::once_flag Singleton<T>::_flag;
template <class T>
Grabo Singleton<T>::_grabo;
#endif // SINGLETON_H
#include <QCoreApplication>
#include <iostream>
#include "singleton.h"
class Keyboard
{
public:
Keyboard(int a = 0, float b = 0.0)
{
std::cout << "Keyboard():" << (a+b) << std::endl;
}
~Keyboard()
{
std::cout << "~Keyboard()" << std::endl;
}
void writeWords()
{
std::cout << "I'm writing! addr : " << (int)this << std::endl;
}
};
int main(int argc, char *argv[])
{
Keyboard* t1 = Singleton<Keyboard>::instance(5, 2.0);
Keyboard* t2 = Singleton<Keyboard>::instance(6, 5.0);
t1->writeWords();
t2->writeWords();
QCoreApplication a(argc, argv);
return a.exec();
}