C++实现Singleton模式(effective c++ 04)

阅读effective c++ 04 (31页) 提到的singleton设计模式。了解一下。

定义:

保证一个类仅有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。

应用场景:

比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息。这种方式简化了在复杂环境下的配置管理。其他还有如系统的日志输出、MODEM的联接需要一条且只需要一条电话线,操作系统只能有一个窗口管理器,一台PC连一个键盘等等。

方式:

根据单例对象创建时间,可分为两种模式:饿汉模式 + 懒汉模式

难点:

难点在于以下四部分,在本文中只说明前两部分。

(1)限制实例数量

(2)线程安全

(3)singleton相互引用

(4)dead-reference

饿汉模式

定义:指全局的单例实例在类装载时构建。

代码:饿汉模式 + 直接定义静态对象 

//.h文件

class Singleton

{

public:

static Singleton& GetInstance();

private:

Singleton(){}

Singleton(const Singleton&);

Singleton& operator= (const Singleton&);

private:

static Singleton m_Instance;

};



//CPP文件

Singleton Singleton::m_Instance;//类外定义-不要忘记写



Singleton& Singleton::GetInstance()

{

return m_Instance;

}



//函数调用

Singleton& instance = Singleton::GetInstance();

优点:实现简单,多线程安全。

缺点:

(1)如果存在多个单例对象且这几个单例对象相互依赖,可能会出现程序崩溃的危险。

(2)在程序开始时,就创建类的实例,如果Singleton对象产生很昂贵,而本身有很少使用,这种方式单从资源利用效率的角度来讲,比懒汉式单例类稍差些。但从反应时间角度来讲,则比懒汉式单例类稍好些。

使用条件:

(1)当肯定不会有构造和析构依赖关系的情况。

(2)想避免频繁加锁时的性能消耗。

相关解释:

(1)多线程下安全:由于类的实例是在类创建的同时就已经创建好,以供系统使用。多个线程线程对该实例的访问是在创建之后,所以安全。

(2)在多个单例对象相互依赖时,程序可能崩溃:对编译器来说,静态成员变量的初始化顺序和析构顺序是一个未定义的行为,也就是对于多个静态对象,先调用哪一个静态对象的构造函数,后调用哪个静态对象的构造函数,编译器也没个准。对应的,静态对象的析构函数的调用顺序也是不定的。所以,若单例实例a的初始化用到单例实例b的值,而实例b有可能在实例a之后初始化,此时程序会引用一个未初始化的内存而出现异常。

说明:

(1)也可以使用静态指针 + 类外初始化时new空间实现。

(2)对于静态变量,需要在类外或者说是Cpp文件中进行定义,分配空间,这个容易忘。

懒汉模式

定义:指全局的单例实例在第一次被使用时构建。

注意:由于实例是在使用时才被创建,因此应该注意多线程的问题。

实现方式有两种:静态指针 + 用到时初始化 和 局部静态变量

实现一:懒汉模式 加 静态指针 加 用到时初始化 加 单线程代码

代码:

//.h文件

class Singleton

{

public:

static Singleton* GetInstance();

private:

Singleton(){}

Singleton(const Singleton&);

Singleton& operator=(const Singleton&);

~Singleton(){}



private:

static Singleton* m_Instance;



class Garbo

{

private:

~Garbo()

{

if (m_Instance)

{

delete m_Instance;

m_Instance = NULL;

cout<<"销毁!"<<endl;

}

}

private:

static Garbo m_Garbo;

};

};



//CPP文件

Singleton* Singleton::m_Instance = NULL;//类外定义



Singleton* Singleton::GetInstance()

{

if (NULL == m_Instance)

{

m_Instance = new Singleton;

}

return m_Instance;

}



//函数调用

Singleton* p1 = Singleton::GetInstance();

Singleton* p2 = p1->GetInstance();

Singleton& p3 = *(Singleton::GetInstance());

说明:

(1)静态类中包含的Garbo类是用来清除静态实例new出来的空间的。

(2)对于空间的释放,还是保留态度。这里写空间的回收只是为了保证代码上的完整性(有new有delete才写的),在下面程序的代码均没写空间释放。

解释:由于静态变量的空间是在全局内存区,其空间的释放是在程序结束才进行释放的。而在程序结束时,系统会自动回收该程序申请的空间。类Garbo的析构函数释放静态实例时,也是在程序结束时才会调用的。所以这里写的内存释放意义不大。当然对于那些在程序结束后不自动回收空间的系统,还是需要写空间回收的。

实现二:懒汉模式 加 局部静态变量 加 单线程代码

//.h文件

class Singleton

{

public:

static Singleton& GetInstance();

private:

Singleton(){};

Singleton(const Singleton&);

Singleton& operator=(const Singleton&);

};


//Cpp实现

Singleton& Singleton::GetInstance()

{

static Singleton instance;

return instance;

}


//函数调用

Singleton& s = Singleton::GetInstance();
  

两种懒汉模式实现方式优缺点分析:

优点:实现简单,用的时候才创建,比较节省。

缺点:

(1)在多线程下不安全。

(2)如果存在多个单例对象的析构顺序有依赖时,可能会出现程序崩溃的危险。

相关解释:

为啥多线程下不安全?

对于语句:

if (NULL == m_Instance)

{

m_Instance = new Singleton;

}

(1)假如有两个线程要访问GetInstance函数,第一个线程进入GetInstance函数,并检测if条件,由于是第一次进入,m_Instance为空,if条件成立,准备执行m_Instance = new Singleton;来创建对象。

(2)但是,有可能被OS的调度器中断,而将控制权交给另外一个线程。

(3)第二个线程同样来到if条件,发现ms_pInstance还是为NULL,因为第一个线程还没来得及构造它就已经被中断了。此时假设第二个线程完成了new的调用,成功的构造了Singleton,并顺利的返回。

(4)之后第一个线程复活,继续执行new再次构造了Singleton,这样一来,两个线程就构建两个Singleton,这就破坏了唯一性。

这里给出静态指针实现懒汉模式的分析,对于局部静态对象的也是一样的。因为 static Singleton instance; 可以分解为多步操作,这里也存在多线程竞争的问题,这里不再解释。

为什么存在多个单例对象的析构顺序有依赖时,可能会出现程序崩溃的危险?

原因:由于静态成员是在第一次调用函数GetInstance时进行初始化,调用构造函数的,因此构造函数的调用顺序时可以唯一确定了。对于析构函数,我们只知道其调用顺序和构造函数的调用顺序相反,但是如果几个Singleton类的析构函数之间也有依赖关系,而且出现类似单例实例A的析构函数中使用了单例实例B,但是程序析构时是先调用实例B的析构函数,此时在A析构函数中使用B时就可能会崩溃。

举例:在李书淦的博客中给出了一个例子,可以参考下:点击打开链接

在文章中也给出了一个解决方法:对于析构的顺序,我们可以用一个容器来管理它,根据单例之间的依赖关系释放实例,对所有的实例的析构顺序进行排序,之后调用各个单例实例的析构方法,如果出现了循环依赖关系,就给出异常,并输出循环依赖环。具体分析见其博客。

masefee的博客中也给出了多个单例模式在析构函数中相互引用时的解决方法,可以瞅瞅。

对于多线程的解决方法:可以使用加锁解决

方法:双检测锁定(double-check),即提高性能,又防止多线程带来的问题

代码:懒汉模式 加 静态指针 加 用到时初始化 加 多线程代码

Singleton* Singleton::GetInstance()

{

if (NULL == m_Instance)

{

lock();

if (NULL == m_Instance)

{

m_Instance = new Singleton;

}

UnLock();

}

return m_Instance;

}
 

分析:

(1)之所以出现多线程的问题,是因为程序在执行if语句块时发生竞争了,最直观的方法就是对整个if语句加锁。代码如下:

Singleton* Singleton::GetInstance()

{

lock();

if (NULL == m_Instance)

{

m_Instance = new Singleton;

}

UnLock();

return m_Instance;

}

这个代码没问题,但是会发现无论是否已经创建过单例实例,之后调用该函数都会造成加锁,而且加锁的代价还是比较昂贵的。其实我们只是在需要创建对象时才需要加锁。因此可以先通过if语句判断下,这样如果已经创建单例实例就不用在加锁了,代码变成:

Singleton* Singleton::GetInstance()

{

if (NULL == m_Instance)

{

lock();

if (NULL == m_Instance)

{

m_Instance = new Singleton;

}

UnLock();

}

return m_Instance;

}

但是,又有大牛指出,编译器可能会对代码进行优化,导致上述代码在DCLP 98标准下是不可靠的,0x标准(11标准)下是可靠的。

给出大牛博客链接:Singleton之C++部分一下面内容是摘抄大牛博客的。再次感谢。

分析如下:

DCLP 就是 Double-checked locking pattern.用于在多线程环境下保证只创建Singleton对象。第一次check不用加锁,但是第二次check和创建对象必须加锁。由于编译器可能会优化代码,导致DCLP模式失效。

在c++98标准下,这是不可靠的。原因有三点:

一,执行顺序得不到保证。编译器会优化代码,从而改变执行顺序。 

m_Instance = new Singleton;这个语句会分成三步完成:

1.分配内存,

2.在已经分配的内存上调用构造函数创建对象,

3.将对象赋值给指针m_Instance .

但是这个顺序很可能会被改变为1,3,2。如果A线程在1,3执行完后,B线程执行第一个条件判断if(m_Instance ==0),此时锁不能起到保护作用。B线程会认为m_Instance 已经指向有效对象,可以去使用了。嘿嘿,灾难发生。主要原因是C++98标准中没有包含多线程,只假定是单线程,编译器的优化行为无视多线程环境,因此产生的优化代码可能会被去掉你的代码或者改变执行顺序。我们没有办法在98标准的采用标准c++语言来解决这个问题,只能采用平台相关的多线程API和与之兼容的编译器来解决。因此,从本质上来说,基于98标准,此问题无解。

二,volatile对于执行顺序也没有帮助。

三,多处理器的系统,B处理器看到变量值的顺序可能和A处理器写变量值的顺序不一致。

小结:造成上述的主要原因是:对代码加锁时,只对里面那个if语句加锁,这就导致当一个线程执行里面的if时,也有可能有线程访问外面的if,来判断实例是否已经创建。但是问题出现在这里了,当代码被优化后,由于锁没加全,使得线程还可以执行外面的if造成的。为了避免这个问题,只能最外面的if也加锁,在创建对象时,我们不允许你检测外面的if。

对于C++98标准下,只能又回到原来的方法,即加简单锁。

代码:懒汉模式 加 静态指针 加 用到时初始化 加 多线程代码 加 C++98标准 

Singleton* Singleton::GetInstance()

{

lock();

if (NULL == m_Instance)

{

m_Instance = new Singleton;

}

UnLock();

return m_Instance;

}
  

对于C++11标准,其包含了多线程,我们可以使用volatile指定代码的指向顺序。因此还是可以使用双检测锁定 + volatile解决多线程问题。

代码:懒汉模式 加 静态指针 加 用到时初始化 加 多线程代码 加 C++11标准 

class Singleton

{

public:

Singleton* Singleton::GetInstance()

{

lock();

if (NULL == m_Instance)

{

m_Instance = new Singleton;

}

UnLock();

return m_Instance;

}

private:

static Singleton * volatile pInstance;//假设关键字volatile

Singleton(){}

Singleton(const Singleton&);

Singleton& operator=(const Singleton&);

~Singleton(){}

};
 

代码:懒汉模式 加 局部静态变量 加 用到时初始化  加 多线程代码 加 C++98标准

 
Singleton& Singleton::GetInstance()

{

Lock();

static Singleton instance;

UnLock();

return instance;

}

由于在C++0x以后,编译器能够保证内部静态变量的线程安全性,可以不加锁。

代码:懒汉模式 加 局部静态变量 加 用到时初始化  加 多线程代码 加 C++11标准

Singleton& Singleton::GetInstance()

{

static Singleton instance;

return instance;

}

在博客的最后,给出boost下singleton模式实现

优点:

(1)在进入main函数前应该是单线程的,可避免了多线程多次初始化的问题。

(2)可避免了静态成员初始化顺序的多样性。

具体代码:boost下singleton模式实现

template <typename T>

struct Singleton

{

struct object_creator

{

object_creator()

{

Singleton<T>::instance();

}

inline void do_nothing()const {}

};

static object_creator create_object;

public:

typedef T object_type;

static object_type& instance()

{

static object_type obj;

create_object.do_nothing();

return obj;

}

};

template <typename T>

typename Singleton<T>::object_creator Singleton<T>::create_object;



// int main()

// {

// int sint = Singleton<int>::instance();

// return 0;

// }
 

代码分析,可以参考fullsail博客:BOOST的Singleton模版详解

参考博客:

单例模式

Singleton之C++部分一

再谈Singleton

【GOF设计模式之路】-- Singleton

BOOST的Singleton模版详解

请高手指点啊——设计模式之单例模式(C++代码实现)

猜你喜欢

转载自blog.csdn.net/weixin_41042404/article/details/81415927