单例模式
单例模式
1、背景
举一个小例子,在我们的windows桌面上,我们打开了一个回收站,当我们试图再次打开一个新的回收站时,Windows系统并不会为你弹出一个新的回收站窗口。,也就是说在整个系统运行的过程中,系统只维护一个回收站的实例。这就是一个典型的单例模式运用。
继续说回收站,我们在实际使用中并不存在需要同时打开两个回收站窗口的必要性。假如我每次创建回收站时都需要消耗大量的资源,而每个回收站之间资源是共享的,那么在没有必要多次重复创建该实例的情况下,创建了多个实例,这样做就会给系统造成不必要的负担,造成资源浪费。
2、定义
保证一个类仅有一个实例,并提供一个访问它的全局访问点。
- 将该类的构造方法定义为私有方法,这样其他处的代码就无法通过调用该类的构造方法来实例化该类的对象,只有通过该类提供的静态方法来得到该类的唯一实例;
- 在该类内提供一个静态方法,当我们调用这个方法时,如果类持有的引用不为空就返回这个引用,如果类保持的引用为空就创建该类的实例并将实例的引用赋予该类保持的引用。
- 线程安全
- 禁止赋值和拷贝
3、特征
1.主要优点
- 1、单例模式提供了对唯一实例的受控访问。因为单例类封装了它的唯一实例,所以它可以严格控制客户怎样以及何时访问它。
- 2、由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象单例模式无疑可以提高系统的性能。
- 3、允许可变数目的实例。基于单例模式我们可以进行扩展,使用与单例控制相似的方法来获得指定个数的对象实例,既节省系统资源,又解决了单例单例对象共享过多有损性能的问题。
2.主要缺点
- 1、由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。
- 2、单例类的职责过重,在一定程度上违背了“单一职责原则”。因为单例类既充当了工厂角色,提供了工厂方法,同时又充当了产品角色,包含一些业务方法,将产品的创建和产品的本身的功能融合到一起。
- 3、现在很多面向对象语言(如Java、C#)的运行环境都提供了自动垃圾回收的技术,因此,如果实例化的共享对象长时间不被利用,系统会认为它是垃圾,会自动销毁并回收资源,下次利用时又将重新实例化,这将导致共享的单例对象状态的丢失。
- 4、滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;
3、通常适用的场景
- 1、在应用场景中,某类只要求生成一个对象的时候,如一个班中的班长、每个人的身份证号等。
- 2、当对象需要被共享的场合。由于单例模式只允许创建一个对象,共享该对象可以节省内存,并加快对象访问速度。如 Web中的配置对象、数据库的连接池、应用程序的日志应用等。
- 该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息
- 3、当某类需要频繁实例化,而创建的对象又频繁被销毁的时候,如多线程的线程池、网络连接池等。
4、应用
4.1、有缺陷的懒汉式
懒汉式的方法,顾名思义,很懒的,配置文件的实例直到用到的时候才会加载。
该模式的特点是类加载时没有生成单例,只有当第一次调用 getlnstance 方法时才去创建这个单例。
#include <iostream>
// version1:
// with problems below:
// 1. thread is not safe
// 2. memory leak
class Singleton{
private:
Singleton(){
std::cout<<"constructor called!"<<std::endl;
}
Singleton(Singleton&)=delete;//=delete禁止使用编译器默认生成的函数
Singleton& operator=(const Singleton&)=delete;
static Singleton* m_instance_ptr;
public:
~Singleton(){
std::cout<<"destructor called!"<<std::endl;
}
static Singleton* get_instance(){
if(m_instance_ptr==nullptr){
m_instance_ptr = new Singleton;
}
return m_instance_ptr;
}
void use() const { std::cout << "in use" << std::endl; }
};
Singleton* Singleton::m_instance_ptr = nullptr;
int main(){
Singleton* instance = Singleton::get_instance();
Singleton* instance_2 = Singleton::get_instance();
return 0;
}
可以看到,获取了两次类的实例,却只有一次类的构造函数被调用,表明只生成了唯一实例,这是个最基础版本的单例实现,他有哪些问题呢?
- 1、线程安全的问题:当多线程获取单例时有可能引发竞态条件:第一个线程在if中判断 m_instance_ptr是空的,于是开始实例化单例;同时第2个线程也尝试获取单例,这个时候判断m_instance_ptr还是空的,于是也开始实例化单例;这样就会实例化出两个对象,这就是线程安全问题的由来;解决办法:
加锁
- 2、内存泄漏. 注意到类中只负责new出对象,却没有负责delete对象,因此只有构造函数被调用,析构函数却没有被调用;因此会导致内存泄漏。解决办法:
使用共享指针
;
4.2、线程安全、内存安全的懒汉式单例 (智能指针,锁)
1、非C++11的加锁实现方式:pthread_mutex_t
class single{
private:
//私有静态指针变量指向唯一实例
static single *p;
//静态锁,是由于静态函数只能访问静态成员
static pthread_mutex_t lock;
//私有化构造函数
single(){
pthread_mutex_init(&lock, NULL);
}
~single(){}
public:
//公有静态方法获取实例
static single* getinstance();
};
pthread_mutex_t single::lock;
single* single::p = NULL;
single* single::getinstance(){
if (NULL == p){
pthread_mutex_lock(&lock);
if (NULL == p){
p = new single;
}
pthread_mutex_unlock(&lock);
}
return p;
}
2、使用C++11智能指针和互斥锁的方式
#include <iostream>
#include <memory> // shared_ptr
#include <mutex> // mutex
// version 2:
// with problems below fixed:
// 1. thread is safe now
// 2. memory doesn't leak
class Singleton{
public:
typedef std::shared_ptr<Singleton> Ptr;
~Singleton(){
std::cout<<"destructor called!"<<std::endl;
}
Singleton(Singleton&)=delete;
Singleton& operator=(const Singleton&)=delete;
static Ptr get_instance(){
//注意这里双重判断
if(m_instance_ptr==nullptr){
std::lock_guard<std::mutex> lk(m_mutex);//C++11加互斥锁,可以自动解锁
if(m_instance_ptr == nullptr){
m_instance_ptr = std::shared_ptr<Singleton>(new Singleton);
}
}
return m_instance_ptr;
}
private:
Singleton(){
std::cout<<"constructor called!"<<std::endl;
}
static Ptr m_instance_ptr;
static std::mutex m_mutex;
};
// initialization static variables out of class
Singleton::Ptr Singleton::m_instance_ptr = nullptr;
std::mutex Singleton::m_mutex;
int main(){
Singleton::Ptr instance = Singleton::get_instance();
Singleton::Ptr instance2 = Singleton::get_instance();
return 0;
}
shared_ptr和mutex都是C++11的标准,以上这种方法的优点是
- 1、基于
shared_ptr
, 用了C++比较倡导的 RAII思想,用对象管理资源,当shared_ptr
析构的时候,new
出来的对象也会被delete
掉。以此避免内存泄漏。 - 2、加了锁,使用互斥量来达到线程安全。这里使用了两个 if判断语句的技术称为双检锁;好处是,只有判断指针为空的时候才加锁,避免每次调用get_instance的方法都加锁,锁的开销毕竟还是有点大的。
为什么需要 “double checked lock”?
对于 Instance
存在的情况,就直接返回,这没有问题。当 Instance为null
并且同时有两个线程调用 GetInstance()
方法时,它们将都可以通过第一重 Instance=null
的判断。然后由于lock
机制,这两个线程则只有一个进入,另一个在外排队等候,必须要其中的一个进入并出来后,另一个才能进入。而此时如果没有了第二重的 Instance
是否为null
的判断,则第一个线程创建了实例,而第二个线程还是可以继续再创建新的实例,这就没有达到单例的目的。
4.3、推荐的懒汉式单例(magic static )——局部静态变量
#include <iostream>
class Singleton
{
public:
~Singleton(){
std::cout<<"destructor called!"<<std::endl;
}
Singleton(const Singleton&)=delete;
Singleton& operator=(const Singleton&)=delete;
static Singleton& get_instance(){
static Singleton instance;
return instance;
}
private:
Singleton(){
std::cout<<"constructor called!"<<std::endl;
}
};
int main(int argc, char *argv[])
{
Singleton& instance_1 = Singleton::get_instance();
Singleton& instance_2 = Singleton::get_instance();
return 0;
}
这是最推荐的一种单例实现方式:
-
1、通过局部静态变量的特性保证了线程安全 (C++11, GCC > 4.3, VS2015支持该特性);
-
2、不需要使用共享指针,代码简洁;
-
3、注意在使用的时候需要声明单例的引用
Single&
才能获取对象。
这时候有人说了,这种方法不加锁会不会造成线程安全问题?
其实,C++0X以后,要求编译器保证内部静态变量的线程安全性,故C++0x之后该实现是线程安全的,C++0x之前仍需加锁,其中C++0x是C++11标准成为正式标准之前的草案临时名字。
4.4、饿汉模式
一旦加载就创建一个单例,保证在调用 getInstance
方法之前单例已经存在了。如果说懒汉式是“时间换空间”
,那么饿汉式就是“空间换时间”
,因为一开始就创建了实例,所以每次用到的之后直接返回就好了。
//全局的单例实例在类装载时构建。
#include <iostream>
using namespace std;
class Singleton
{
private:
static Singleton* pInstance;
Singleton()
{
cout << "constructor called!" << std::endl;
}
Singleton(const Singleton&) = delete;
Singleton& operator = (const Singleton&) = delete;
public:
static Singleton* getInstance();
~Singleton() {
std::cout << "destructor called!" << std::endl;
}
void print()
{
cout << "this = " << this << endl;
}
};
Singleton* Singleton::pInstance = new Singleton;全局的单例实例在类装载时构建。
// 静态方法返回该实例
Singleton* Singleton::getInstance()
{
return pInstance;
}
int main()
{
Singleton* s1 = Singleton::getInstance();
Singleton* s2 = Singleton::getInstance();
s1->print();
s2->print();
return 0;
}
优点:
饿汉式单例在类创建的同时就已经创建好一个静态的对象供系统使用,以后不再改变,所以是线程安全的,可以直接用于多线程而不会出现问题。
缺点:
当类Singleton
被加载的时候,会初始化static的instance
,静态变量被创建并分配内存空间,从这以后,这个static的instance
对象便一直占着这段内存
(即便你还没有用到这个实例),当类被卸载时,静态变量被摧毁,并释放所占有的内存,因此在某些特定条件下会耗费内存。
参考
1、https://www.cnblogs.com/xiaoxi/p/7799456.html
2、http://c.biancheng.net/view/1338.html
3、https://www.cnblogs.com/sunchaothu/p/10389842.html