目录
概述
单例模式是创建型设计模式中的一种,创建型模式它主要是用来管理对象的分配和释放;单例模式又称单件模式,实质是一种经过改进的全局变量;保证一个class只有一个实体(instance),并为它提供一个全局的访问点。有很多地方需要这样的功能模块,如系统的日志输出,,MODEM的联接需要一条且只需要一条电话线,操作系统只能有一个窗口管理器,只有一个系统时钟,一台PC连一个键盘。UML通用类图如下:
对于C++各种单例模式的设计与实现,主要涉及到以下几点:
1.与一般单纯的全局函数相比,C++用静态成员变量和静态成员函数实现了单例对象,静态 变量存放在全局存储区,且是唯一的,供所有对象使用。
2.用于支持单例的一些C++基本手法,如默认构造函数和复制构造函数私有化,阻止了外界用户私自创建对象,阻止了多实例的发生。
3.单例模式对象的创建方式,如懒汉式和饿汉式,懒汉式是程序中需要此对象的才去创建它;饿汉式是对象就像全局变量一样,进入main函数之前,程序就自动创建了它。
4.摧毁对象并检测摧毁之后的访问动作。
一个合理的Singletons至少应该执行"dead-reference检测",为了做到这一点,我们可以增加一个成员变量static bool destroyed_来追踪析构行为,其值一开始为false,Singletons析构函数会将它设为true,具体的代码如下:
cpp
class CSingletons
{
private:
CSingletons() {}
CSingletons(const CSingletons&){}
CSingletons& operator=(const CSingletons&){}
public:
static CSingletons* getInstance(){
if (!m_pInstance){
onDeadReference();
}else{
Create();
}
return m_pInstance;
}
private:
void Create(){
static CSingletons theInstance;
m_pInstance= &theInstance;
}
static void onDeadReference(){
throw std::runtime_error("Dead reference detacted");
}
~CSingletons(){
m_pInstance = 0;
m_destoryed = true;
}
private:
static CSingletons* m_pInstance;
static bool m_destoryed;
}
static CSingletons* CSingletons::m_pInstance= 0;
static bool CSingletons::m_destoryed = false;
5.实现单例模式对象的生命周期管理方案,可以使用生命周期的优先级来解决这个问题。
6.多线程相关问题。
构造函数私有化方式
禁止外界用户私自创建class的方式有两种:
1.声明private
如下代码:
cpp
class CSingletons
{
private:
CSingletons();
~CSingletons();
CSingletons(const CSingletons&);
CSingletons& operator=(const CSingletons&);
public:
...
}
2.=delete
在C++11及其后续版本中,我们可以在函数原型中使用=delete
关键字,这是一种显式地禁止某个函数的使用的方式。这种技术常常用于禁止类的复制操作,或者阻止特定类型的参数。代码如下:
cpp
class CSingletons
{
public:
CSingletons()=delete;
~CSingletons()=delete;
CSingletons(const CSingletons&)=delete;
CSingletons& operator=(const CSingletons&)=delete;
public:
...
}
实现方式
1.静态局部变量方式
C++11后,规定了局部静态对象在多线程场景下的初始化行为,只有在首次访问时才会创建实例,后续不再创建而是获取。若未创建成功,其他的线程在进行到这步时会自动等待。注意C++11前的版本不是这样的,如下代码:
cpp
class CSingletons
{
private:
CSingletons() {}
~CSingletons(){}
CSingletons(const CSingletons&){}
CSingletons& operator=(const CSingletons&){}
public:
static CSingletons* getInstance(){
static CSingletons obj; //1
return &obj;
}
}
1处看似很简单的一行代码,其实编译器背后在此处会产生很多代码,使得初始化之后,执行期相关机制会登记需被析构的变量,这写代码的C++形式的伪码(pseudo code)看起来像下面这样(两个底线起头的变量应该被视为影藏变量,也就是由编译器产生和管理的变量):
cpp
CSingletons& CSingletons::instance(){
extern void __ConstructSingletons(void* memory);
extern void __DestroySingletons();
static bool __initialized = false;
static char __buffer[sizeof(CSingletons)];
if (!__initialized ){
__ConstructSingletons(__buffer);
atexit(__DestroySingletons);
__initialized = true;
}
return *reinterpret_cast<CSingletons*>(__object);
}
其中的核心动作是对atexit()的调用,atexit()由标准C程序库提供,让你得以注册一些在程序结束之际自动被调用的函数,且其被调用次序为后进先出(LIFO,根据定义,C++对象析构已LIFO方式进行,先产生的对象后摧毁)。
2.静态成员变量方式
这种方式和全局变量方式差不多,代码如下:
cpp
class CSingletons
{
private:
CSingletons() {}
~CSingletons(){}
CSingletons(const CSingletons&){}
CSingletons& operator=(const CSingletons&){}
public:
static CSingletons* getInstance(){
return &m_obj;
}
private:
static CSingletons m_obj;
}
static CSingletons CSingletons::m_obj;
3.双重检测指针和atexit方式
通过双重检测锁,可以确保线程安全。代码如下:
cpp
#include <mutex>
using namespace std;
class CSingletons
{
private:
CSingletons() {}
~CSingletons(){}
CSingletons(const CSingletons&){}
CSingletons& operator=(const CSingletons&){}
public:
static CSingletons* getInstance(){
if (m_pObj == 0){ //1
std::unique_lock locker(m_mutex);//加互斥锁
if (m_pObj == 0){ //2
m_pObj = new CSingletons();
atexit(CSingletons::destroyInstance);
}
}
return m_pObj;
}
private:
static void destroyInstance(){
if (m_pObj){
delete m_pObj;
}
}
private:
static CSingletons* m_pObj;
static std::mutex m_mutex;
}
static CSingletons* CSingletons::m_pObj = 0;
static std::mutex CSingletons::m_mutex;
在代码的1处如果两个线程同时进行到这一步,一个线程继续向下执行时,另一个线程被堵塞;等锁解除后,被堵塞的线程就会跳过2处了,因为此时实例已经构建完毕。
上面的代码之所以在getInstance函数里面对m_pObj是否为空做了两次判断,因为该方法调用一次就产生了对象,m_pObj== NULL 大部分情况下都为false,如果按照原来的方法,每次获取实例都需要加锁,效率太低。而改进的方法只需要在第一次 调用的时候加锁,可大大提高效率。
4.双重检测指针和自定义销毁器方式
其实在CSingletons内部也可以自定义内部类,利用析构函数删除m_pObj,代码如下:
cpp
#include <mutex>
using namespace std;
class CSingletons
{
private:
CSingletons() {}
~CSingletons(){}
CSingletons(const CSingletons&){}
CSingletons& operator=(const CSingletons&){}
public:
static CSingletons* getInstance(){
if (m_pObj == 0){ //1
std::unique_lock locker(m_mutex);//加互斥锁
if (m_pObj == 0){ //2
m_pObj = new CSingletons();
}
}
return m_pObj;
}
private:
class MyDeleter{
public:
~MyDeleter(){
if (m_pObj){
delete m_pObj;
}
}
}
private:
static CSingletons* m_pObj;
static std::mutex m_mutex;
static MyDeleter m_deleter;
}
static CSingletons* CSingletons::m_pObj = 0;
static std::mutex CSingletons::m_mutex;
static CSingletons::MyDeleter CSingletons::m_deleter;
5.智能指针方式
智能指针的最大好处就是能够自动释放内存,这里用std::shared_ptr,代码如下:
cpp
#include <mutex>
using namespace std;
class CSingletons
{
private:
CSingletons() {}
~CSingletons(){}
CSingletons(const CSingletons&){}
CSingletons& operator=(const CSingletons&){}
public:
static std::shared_ptr<CSingletons> getInstance(){
if (!m_pObj){ //1
std::unique_lock locker(m_mutex);//加互斥锁
if (!m_pObj){ //2
m_pObj.reset(new CSingletons());
}
}
return m_pObj;
}
private:
static std::shared_ptr<CSingletons> m_pObj;
static std::mutex m_mutex;
}
static std::shared_ptr<CSingletons> CSingletons::m_pObj = 0;
static std::mutex CSingletons::m_mutex;
6.智能指针和自定义销毁器方式
智能指针的销毁也可以自定义,如下代码:
cpp
#include <mutex>
using namespace std;
class CSingletons
{
private:
CSingletons() {}
~CSingletons(){}
CSingletons(const CSingletons&){}
CSingletons& operator=(const CSingletons&){}
public:
static std::shared_ptr<CSingletons> getInstance(){
if (!m_pObj){ //1
std::unique_lock locker(m_mutex);//加互斥锁
if (!m_pObj){ //2
m_pObj.reset(new CSingletons(), [](CSingletons* p){
if (p){
delete p;
}
});
}
}
return m_pObj;
}
private:
static std::shared_ptr<CSingletons> m_pObj;
static std::mutex m_mutex;
}
static std::shared_ptr<CSingletons> CSingletons::m_pObj = 0;
static std::mutex CSingletons::m_mutex;
关于自定义销毁器还有不明白的地方,可以查看我的另外一篇博客C++智能指针的自定义销毁器(销毁策略)_51如何销毁指针-CSDN博客
7.Qt的原子指针方式
代码如下:
cpp
#include <mutex>
using namespace std;
class CSingletons
{
private:
CSingletons() {}
~CSingletons(){}
CSingletons(const CSingletons&){}
CSingletons& operator=(const CSingletons&){}
public:
static CSingletons& getInstance(){
//使用双重检测。
/*! testAndSetOrders操作保证在原子操作前和后的的内存访问
* 不会被重新排序。
*/
if (m_instance.testAndSetOrdered(0, 0))//第一次检测
{
QMutexLocker locker(&mutex);//加互斥锁。
m_instance.testAndSetOrdered(0, new SingleTon());//第二次检测。
}
return *m_instance;
}
private:
static QMutex m_mutex;//实例互斥锁。
static QAtomicPointer<SingleTon> m_instance;/*!<使用原子指针,默认初始化为0。*/
}
static QAtomicPointer<SingleTon> CSingletons::m_instance;
static QMutex CSingletons::m_mutex;
单例模式与控制反转
显式地将某个组件变为单例的方式具有明显的侵入性,而如果决定在某一时段不再将某个类作为单例,最终又会付出高昂的代价。另一种解决方案是采用一种约定,在这种约定中,负责组件的函数并不直接控制组件的生命周期,而是外包给控制反转(Inversion of Control, IoC)容器。
当使用Boost.DI的依赖注入框架时,定义单例组件的代码如下:
cpp
auto injector = di::make_injector(
di::bind<IFoo>.to<Foo>.in(di::singleton),
//other configuration steps here
);
//Foo继承IFoo
从上面的代码中,我们使用字母"I"表示接口类型。本质上,di::bind这一行代码的意思是,每当需要具有IFoo类型成员的组件时,我们使用Foo的单例实例来初始化该组件。
许多开发人员认为,在DI容器中使用单例是唯一可以接受的使用单例的方式。至少,如果需要用其他东西替换单例,使用这种方式就是可以在一个中心位置(配置容器的代码处)执行这个操作。另外一个好处是,我们不必自己实现任何单例的逻辑,这可以防止出现潜在的错误。此外,是否提到过Boost.DI是线程安全的?
优点
1)由于单例模式只生成一个实例,所以减少了系统的性能开销,当一个对象的产生需要比较多的资源时,如读取配置、产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后用永久驻留内存的方式来解决。
2)由于单例模式在内存中只有一个实例,减少了内存开支,特别是一个对象需要频繁地创建、销毁时,而且创建或销毁时性能又无法优化,单例模式的优势就非常明显。
3)单例模式可以避免对资源的多重占用,例如一个写文件动作,由于只有一个实例存在内存中,避免对同一个资源文件的同时写操作。
4)单例模式可以在系统设置全局的访问点,优化和共享资源访问,例如可以设计一个单例类,负责所有数据表的映射处理。
缺点
1)单例模式一般没有接口,扩展很困难,若要扩展,除了修改代码基本上没有第二种途径可以实现。单例模式为什么不能增加接口呢?因为接口对单例模式是没有任何意义的,它要求"自行实例化",并且提供单一实例、接口或抽象类是不可能被实例化的。当然,在特殊情况下,单例模式可以实现接口、被继承等,需要在系统开发中根据环境判断。
2)单例模式对测试是不利的。在并行开发环境中,如果单例模式没有完成,是不能进行测试的,没有接口也不能使用mock的方式虚拟一个对象。
3)单例模式与单一职责原则有冲突。一个类应该只实现一个逻辑,而不关心它是否是单例的,是不是要单例取决于环境,单例模式把要单例和业务逻辑融合在一个类中。
使用场景
在一个系统中,要求一个类有仅有一个对象,如果出现多个对象就会出现"个良反应",可以采用单例模式,具体的场景如下:
- 数据库连接池,在多线程环境下,通过单例模式管理数据库连接池可以避免资源的重复创建和释放,提高性能和效率
2)日志记录器,使用单例模式管理日志记录器可以确保日志的一致性,并提供全局的访问点,方便记录系统中的日志信息
3)在整个项目中需要一个共享访问点或共享数据,例如一个web页面上的计数器,可以不用把每次刷新都记录到数据库中,使用单例模式保持计数器的值,并确保是线程安全的。创建一个对象需要消耗的资源过多,如要访问IO和数据库等资源。
4)需要定义大量的静态常量和静态方法(如工具类)的环境,可以采用单例模式(当然,也可以直接声明为static的方式)。
原创不易,如果觉得喜欢,可以收藏和关注!!!