一、单例模式概念
1、单例模式,是一种常用的软件设计模式。在它的核心结构中只包含一个被称为单例的特殊类。通过单例模式可以保证系统中,应用该模式的类一个类只有一个实例。即一个类只有一个对象实例
二、单例模式的组成
1、定义一个静态的GetInstance方法,返回该类的唯一实例。
2、类定义一个自身的静态成员变量,只有当该类未实例化的时候创建该类的实例。
3、将类的构造函数定义成私有的,不允许外界访问构造函数,创建类的实例。
单例模式代码如下
#include <iostream>
using namespace std;
class Singleton
{
public:
void show()
{
cout << "This is a singleton class" << endl;
}
static Singleton* GetInstance()
{
if (m_singleton == NULL)
m_singleton = new Singleton();
return m_singleton;
}
private:
Singleton(){}
static Singleton* m_singleton;
};
Singleton* Singleton::m_singleton = NULL;
客户端代码
int main()
{
Singleton* s1 = Singleton::GetInstance();
Singleton* s2 = Singleton::GetInstance();
if (s1 == s2)
s1->show();
return 0;
}
上面的代码还是有一些问题的比如
上面的类仍然可以通过拷贝构造和赋值运算符重载来生成多个实例
int main()
{
Singleton* s1 = Singleton::GetInstance();
Singleton s3(*s1);
system("pause");
return 0;
}
要真正解决全局单一实例需要将类的拷贝构造函数和赋值运算符重载函数声明出来,并且不定义,而且要设置为私有或者保护,这样就不会在类外进行定义。
class Singleton
{
public:
void show()
{
cout << "This is a singleton class" << endl;
}
static Singleton* GetInstance()
{
if (m_singleton == NULL)
m_singleton = new Singleton();
return m_singleton;
}
private:
Singleton(){}
//只声明不定义,声明在保护或者私有
Singleton(Singleton&); //拷贝构造
Singleton& operator=(Singleton&); //赋值运算符重载
static Singleton* m_singleton;
};
上面方法就解决了单一实例的问题,但是还是存在一些线程安全的问题
class Singleton
{
public:
static Singleton* GetInstance()
{
//thread1
//thread2
if (m_singleton == NULL)
m_singleton = new Singleton();
return m_singleton;
}
private:
Singleton(){}
Singleton(Singleton&);
Singleton& operator=(Singleton&);
static Singleton* m_singleton;
};
当上面两个线程同时执行到GetInstance()时,由于m_singleton = new Singleton();这一行C++代码是由多条指令组成,比如折行代码分成了三部分,先开辟空间(多指令),调构造函数(多指令),赋值(多指令),这里只能保证执行一句指令是原子的。thread1当代码执行多条指令后,但是没有执行到赋值的指令,就是m_singleton还是的NULL的时候,这个线程的时间片完成了,就要执行另外一个线程thread2,thread2认为m_singleton为空,所以就继续new一个对象,将原来的对象覆盖掉,这样讲相当于创建了多个实例对象,危险的是第一个线程new出来的对象没有被销毁,造成内存泄漏的问题。
如何解决上述线程安全的问题呢
在Linux里面我们有lock() unlock()
第一个线程执行加了锁的代码后,其他线程就不能进入这段代码,只有第一个线程执行完解锁,之后的线程才能使用,到时候m_singleton已经被改变不为NULL,所以之后的线程不再创建新的对象
thread1、thread2
lock();
if (m_singleton == NULL)
{
m_singleton = new Singleton();
}
unlock();
但是在windows下没有lock() 和unlock()
C++11中有互斥锁文件< mutex>使用参开C++文件,下面展示该处的使用
#include <iostream>
#include <mutex>
using namespace std;
class Singleton
{
public:
void show()
{
cout << "This is a singleton class" << endl;
}
static Singleton* GetInstance()
{
_mtx.lock(); //
if (m_singleton == NULL)
{
m_singleton = new Singleton();
}
_mtx.unlock(); //
return m_singleton;
}
private:
static mutex _mtx;
Singleton(){}
static Singleton* m_singleton;
};
//静态成员变量在类外定义
//不用对互斥变量赋值,因为有缺省的构造函数
mutex Singleton::_mtx;
//定义一个互斥变量,在类里面定义,保证多个线程用的是一个锁,
//这样才起到加锁的作用,不能定义在函数内部,定义在函数内部的
//在栈空间,每个线程都有自己的栈空间,多个线程多个锁之间不起作用
//静态函数只能访问静态的成员变量
终于解决了线程安全的问题,但是不要着急,虽然解决了线程安全的问题,但是上面的代码不是很高效,因为上面的代码只需要线程第一次来的时候加锁,之后代码被改变m_singleton不为空了,也就不需要加锁了,多线程支持多读单写的,一个线程可以改变m_singleton,但是不影响其他线程的读if (m_singleton == NULL)的操作,加锁后会影响效率的问题,所以我们使用双重检查(提高效率)
if(m_singleton == NULL)
{
lock();
if (m_singleton == NULL)
{
m_singleton = new Singleton();
}
unlock();
}
所以只有第一次执行的时候才加锁,之后被外边的语句检测,不需要加锁,减少了开销。
上面还是存在一些问题的,new和构造函数都可能会跑异常,这样的话m_singleton还是NULL,只要来调用GetInstance的线程全部阻塞在这里,会造成死锁,因为第一线程被有解锁,程序就冻结起来了,
1、这个情况就需要捕获异常进行解锁,
2、或者自己定义一个类,构造函数里面加锁,析构函数解锁
3、使用RAII方式,C++11中的lock_guard lock(_mtx);
class Singleton
{
public:
void show()
{
cout << "This is a singleton class" << endl;
}
static Singleton* GetInstance()
{
//RAII方式
lock_guard<mutex> lock(_mtx);
if (m_singleton == NULL)
{
m_singleton = new Singleton();
}
return m_singleton;
}
private:
static mutex _mtx;
Singleton(){}
static Singleton* m_singleton;
}
出了这一块作用域,自动解锁
懒汉模式 第一次调用对象的时候创建实例(上面的代码全是这个模式)
饿汉模式 。。。
#include <iostream>
#include <cassert>
using namespace std;
class Singleton
{
public:
void show()
{
cout << "This is a singleton class" << endl;
}
static Singleton* GetInstance()
{
assert(m_singleton);
return m_singleton;
}
private:
Singleton(){}
static Singleton* m_singleton;
};
//饿汉模式提前创建一个对象,当使用的时候直接调用
Singleton* Singleton::m_singleton = new Singleton;
//客户端代码
int main()
{
Singleton* s1 = Singleton::GetInstance();
Singleton* s2 = Singleton::GetInstance();
if (s1 == s2)
s1->show();
return 0;
}
也可以这种方式
定义一个静态的对象(最开始就已经创建好),返回引用
class Singleton
{
public:
void show()
{
cout << "This is a singleton class" << endl;
}
static Singleton* GetInstance()
{
static Singleton tmp;
return &tmp;
}
private:
Singleton(){}
};
三、单例模式相关的问题
1、定义一个类只能在堆上生成对象(说明对象时new出来的)
class Singleton
{
public:
static Singleton* GetInstance()
{
m_singleton = new Singleton();
return m_singleton;
}
private:
Singleton(){}
static Singleton* m_singleton;
};
2、设计一个类只能在栈上生成对象
。。。
四、单例模式的优缺点
1、优点
- 提供了对唯一实例的的后空访问,避免对资源的多重占用。
- 在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例。
- 缩小了命名空间,避免全局变量污染空间,但比类操作更灵活
- 单例模式可以允许可变的数目的实例,使用单利模式进行扩展,使用控制单利对象相似的方法获取指定个数的实例,及解决了单利对象,共享过多,而有损性能的问题;
2、缺点
- 由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。
- 单例类的职责过重,在一定程度上违背了“单一职责原则”。
- 滥用单例将带来一些负面的问题,如为了节省资源将数据库连接池对象设计为单例模式,可能会导致共享连接池对象的程序过多未出而出现的连接池溢出,如果实例化对象长时间不用系统就会被认为垃圾对象被回收,这将导致对象状态丢失。