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模版详解
参考博客:
C++实现Singleton模式(effective c++ 04)的更多相关文章
- 人工智能——Singleton模式
上次在状态模式中的设计有一个严重的问题,就是如下: voidCTroll::ChageState(CState* pNewState) { deletem_pCurrentState; ...
- C++内存管理(effective c++ 04)
阅读effective c++ 04 (30页) 提到的static对象和堆与栈对象.看了看侯老师的内存管理视频1~3.有点深. 了解一下. 目录 1 内存管理 1.1 C++内存管理详解 1.1.1 ...
- 【设计模式】Singleton模式C++实现
Singleton是设计模式中比较简单的一个.园中的朋友们应该都很熟悉了.前段时间参加xxx外企的面试,和面试官讨论C++的时候正好写了一个.当时由于在有些地方考虑不太周全,代码出现了一些疏漏.不过最 ...
- Singleton模式C++实现
Singleton模式C++实现 Singleton是设计模式中比较简单的一个.园中的朋友们应该都很熟悉了.前段时间参加xxx外企的面试,和面试官讨论C++的时候正好写了一个.当时由于在有些地方考虑不 ...
- 面试:用 Java 实现一个 Singleton 模式
面试:用 Java 实现一个 Singleton 模式 面试系列更新后,终于迎来了我们的第一期,我们也将贴近<剑指 Offer>的题目给大家带来 Java 的讲解,个人还是非常推荐< ...
- 剑指Offer对答如流系列 - 实现Singleton模式
目录 面试题2:实现Singleton模式 一.懒汉式写法 二.饿汉式写法 三.枚举 面试题2:实现Singleton模式 题目:设计一个类,我们只能生成该类的一个实例. 由于设计模式在面向对象程序设 ...
- Qt 中使用Singleton模式需小心
在qt中,使用Singleton模式时一定要小心.因为Singleton模式中使用的是静态对象,静态对象是直到程序结束才被释放的,然而,一旦把该静态对象纳入了Qt的父子对象体系,就会导致不明确的行为. ...
- 剑指Offer面试题:1.实现Singleton模式
说来惭愧,自己在毕业之前就该好好看看<剑指Offer>这本书的,但是各种原因就是没看,也因此错过了很多机会,后悔莫及.但是后悔是没用的,现在趁还有余力,把这本书好好看一遍,并通过C#通通实 ...
- C++ Singleton模式
地址:http://www.cppblog.com/dyj057/archive/2005/09/20/346.html Singleton模式是常用的设计模式之一,但是要实现一个真正实用的设计模式却 ...
随机推荐
- 坑爹的 Java 可变参数,把我整得够惨。。
最近在写一个功能点,用了 Java 中的可变参数,真是把我搞得够惨.. 什么是可变参数? 就是方法参数用 Object... args 三个点形式,一个参数可以接收多个参数. 实际的代码就不帖了,来看 ...
- Java基础--基本规则、语法
一.关键字.保留字.标识符.常量.变量 1.关键字:使用某种语言赋予特殊含义的单词. 2.保留字:没有赋予特殊含义,但以后可能会使用的单词. 3.标识符:自定义的单词,以数字.字母.下划线以及$符组成 ...
- Java程序动态编译Java源文件
最近接触到公司一个项目,需要将生成的源码动态编译,记录下学习过程. 先贴出官网推荐写法: JavaCompiler.CompilationTask getTask(Writer out, ...
- 12pm 究竟是中午还是午夜
12pm是中午=12noon12am是午夜=12midnightMN-midnight(午夜,中午) AM是after midnight开头字母 PM是prior to midnight开头字母正中午 ...
- Django模板语言,标签整理
Django模板语言 标签 内置标签引用 1. autoescape 控制自动转义是否可用. 这种标签带有任何 on 或 off 作为参数的话,他将决定转义块内效果. 该标签会以一个endautoes ...
- Django中间件的执行流程
Django中间件的执行流程. 请求到达中间件之后,先按照正序执行每个注册中间件的process_reques方法,process_request方法返回的值是None,就依次执行, 如果返回的值是H ...
- Yii2.0权限系统,使用PhpManager的方式
网上搜了一大堆yii2.0权限系统,大抵都是千篇一律,而且基本上都是DbManager.看了半天官方文档之后,终于知道了PhpManager的方式下,是怎么引入权限系统.介绍下我自己的使用.首先,配置 ...
- linux替换文件中的某个字符串的命令sed
sed -i 's/java-7-oracle/java-8-oracle/g' /etc/init.d/tomcat7 上面的命令是将tomcat7中的java-7-oracle替换为java-8- ...
- Hart协议
官方https://fieldcommgroup.org/technologies/hart/documents-and-downloads-hart 参考网页http://www.eeworld.c ...
- ef 操作 mysql 中文乱码问题
1.保证mysql数据的编码为utf8 启动mysql mysql -hlocalhost -uroot -p 输入密码 show VARIABLES like 'character_%'; SET ...