单例模式
单例模式
1、什么是单例模式
单例模式是指在内存中只会创建且仅创建一次对象的设计模式。
在程序中多次使用同一个对象且作用相同时,为了防止频繁地创建对象使得内存飙升,单例模式可以让程序仅在内存中创建一个对象,让所有需要调用的地方都共享这一单例对象。
2、单例模式的特征和应用场景
1.主要优点
- 1、单例模式提供了对唯一实例的受控访问。因为单例类封装了它的唯一实例,所以它可以严格控制客户怎样以及何时访问它。
- 2、由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象单例模式无疑可以提高系统的性能。
- 3、允许可变数目的实例。基于单例模式我们可以进行扩展,使用与单例控制相似的方法来获得指定个数的对象实例,既节省系统资源,又解决了单例单例对象共享过多有损性能的问题。
2.主要缺点
- 1、由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。
- 2、单例类的职责过重,在一定程度上违背了“单一职责原则”。因为单例类既充当了工厂角色,提供了工厂方法,同时又充当了产品角色,包含一些业务方法,将产品的创建和产品的本身的功能融合到一起。
- 3、现在很多面向对象语言(如Java、C#)的运行环境都提供了自动垃圾回收的技术,因此,如果实例化的共享对象长时间不被利用,系统会认为它是垃圾,会自动销毁并回收资源,下次利用时又将重新实例化,这将导致共享的单例对象状态的丢失。
- 4、滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;
3、通常适用的场景
- 1、在应用场景中,某类只要求生成一个对象的时候,如一个班中的班长、每个人的身份证号等。
- 2、当对象需要被共享的场合。由于单例模式只允许创建一个对象,共享该对象可以节省内存,并加快对象访问速度。如 Web中的配置对象、数据库的连接池、应用程序的日志应用等。
- 该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息
- 3、当某类需要频繁实例化,而创建的对象又频繁被销毁的时候,如多线程的线程池、网络连接池等。
3、单例模式的实现方式
单例模式有两种类型:
懒汉式:
在真正需要使用对象时才去创建该单例类对象饿汉式
:在类加载时已经创建好该单例对象,等待被程序使用
特点与选择
懒汉式是以时间换空间,适应于访问量较小时;推荐使用内部静态变量的懒汉单例,代码量少
饿汉式是以空间换时间,适应于访问量较大时,或者线程比较多的的情况
3.1、懒汉式创建单例对象
懒汉式创建对象的方法是在程序使用对象前,先判断该对象是否已经实例化(判空),若已实例化直接返回该类对象。,否则则先执行实例化操作。
3.1.1、懒汉式如何保证只创建一个对象
static Singleton* get_instance(){
if(m_instance_ptr==nullptr){
m_instance_ptr = new Singleton;
}
return m_instance_ptr;
}
这个方法其实是存在问题的,试想一下,如果两个线程同时判断singleton为空,那么它们都会去实例化一个Singleton对象,这就变成双例了。所以,我们要解决的是线程安全问题。
最容易想到的解决方法就是在方法上加锁,或者是对类对象加锁,程序就会变成下面这个样子
typedef std::shared_ptr<Singleton> Ptr;
static Ptr get_instance(){
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;
}
这样就规避了两个线程同时创建Singleton对象的风险,但是引来另外一个问题:每次去获取对象都需要先获取锁,并发性能非常地差,极端情况下,可能会出现卡顿现象
。
接下来要做的就是优化性能,目标是:如果没有实例化对象则加锁创建,如果已经实例化了,则不需要加锁,直接获取实例
所以直接在方法上加锁的方式就被废掉了,因为这种方式无论如何都需要先获取锁
typedef std::shared_ptr<Singleton> Ptr;
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;
}
因为需要两次判空,且对类对象加锁,该懒汉式写法也被称为:Double Check(双重校验) + Lock(加锁)
第3行代码
,如果singleton不为空,则直接返回对象,不需要获取锁;而如果多个线程发现singleton为空,则进入分支;第一次判断的作用
第4行代码
,多个线程尝试争抢同一个锁,只有一个线程争抢成功,第一个获取到锁的线程会再次判断singleton是否为空,因为singleton有可能已经被之前的线程实例化- 其它之后获取到锁的线程在执行到
第5行校验代码
,发现singleton已经不为空了,则不会再new一个对象,直接返回对象即可。第二次判断的作用
- 之后所有进入该方法的线程都不会去获取锁,在第一次判断singleton对象时已经不为空了
完整代码如下
#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;
}
3.1.2、懒汉式如何在不使用锁机制的情况下保证线程安全
-std=c++0x编译是使用了C++11的特性,C++0X以后,要求编译器保证内部静态变量的线程安全性
,可以不加锁。但C++ 0X以前,仍需要加锁。
#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&
才能获取对象。
3.2、饿汉式创建单例对象
饿汉式在类加载时已经创建好该对象,在程序调用时直接返回该单例对象即可
,即我们在编码时就已经指明了要马上创建这个对象,不需要等到被调用时再去创建。
```csharp
//全局的单例实例在类装载时构建。
#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;
}
饿汉模式不用在考虑线程安全问题了?
- 即无论是否调用该类的实例,在程序开始时就会产生一个该类的实例,并在以后仅返回此实例。
- 由静态初始化实例保证其线程安全性,WHY?因为静态实例初始化在程序开始时进入主函数之前就由主线程以单线程方式完成了初始化,不必担心多线程问题。
- 也就是说:在类创建的同时就已经创建好一个静态的对象供系统使用,以后不再改变,所以是线程安全的,可以直接用于多线程而不会出现问题。
- 故在性能需求较高时,应使用这种模式,避免频繁的锁争夺。
4、 总结
(1)单例模式有两种:懒汉式、饿汉式
(2)懒汉式:在需要用到对象时才实例化对象,正确的实现方式是:Double Check + Lock,解决了并发安全和性能低下问题
(3)饿汉式:在类加载时已经创建好该单例对象,在获取单例对象时直接返回对象即可,不会存在并发安全和性能问题。
(4)在开发中如果对内存要求非常高,那么使用懒汉式写法,可以在特定时候才创建该对象;
(5)如果对内存要求不高使用饿汉式写法,因为简单不易出错,且没有任何并发安全和性能问题
参考
1、https://zhuanlan.zhihu.com/p/160842212