C++ 单例模式深度剖析
文章目录
单例是最常用的一种设计模式,实现方法多样,根据不同的需求有不同的写法; 同时单例也有局限性,因此我们在使用单例时需要斟酌。接下来我们用C++为单例的常见写法做一个全面的总结, 包括懒汉式、饿汉式、线程安全、单例模板等; 按照从简单到复杂,最终回归简单的的方式循序渐进地介绍,并且对各种实现方法的局限进行详细的描述,用到了智能指针, 线程锁等知识;
一、什么是单例
单例模式是设计模式的一种,其特点是只提供唯一一个类的实例,具有全局变量的特点,在任何位置都可以通过接口获取到的唯一实例;
■ 使用场景
- 如果系统中只有唯一一个实例存在的类的全局变量的时候才使用单例。
- 多线程的线程池的设计一般也是采用单例模式,这是由于线程池要方便对池中的线程进行控制。
- 操作系统的文件系统,也是大的单例模式实现的具体例子,一个操作系统只能有一个文件系统。
- windows的Recycle Bin(回收站)也是典型的单例应用,在整个系统运行过程中,回收站一直维护着仅有的一个实例。
- 对单例的要求:
越简单越小越好,线程安全,内存不泄露
■ 1.1 单例模式分类
单例模式可以分为懒汉式和饿汉式,区别在于创建实例的阶段不同:
- 懒汉式(线程不安全):程序运行时,当需要使用该实例时,才创建并使用实例。
- 饿汉式(线程安全):程序启动时就创建实例并初始化,当需要时直接调用即可。
■ 1.2 单例类特点
- 构造函数和析构函数为private类型,禁止外部构造和析构
- 拷贝构造和赋值构造函数为private类型,目的是禁止外部拷贝和赋值,确保实例的唯一性
- 类中有一个获取实例的静态函数getInstance,可以全局调用
二、C++单例的实现
■ 2.1 基础要点
- 全局只有一个实例:static 特性,同时禁止用户自己声明并定义实例
- 线程安全
- 禁止赋值和拷贝
- 用户通过接口获取实例:使用 static 类成员函数
- 原因如下:
1.通过静态的类方法(getInstance) 获取instance,该方法是静态方法,instance由该方法返回(被该方法使用),如果instance非静态,无法被getInstance调用;
2.instance需要在调用getInstance时候被初始化,只有static的成员才能在没有创建对象时进行初始化。并且类的静态成员在类第一次被使用时初始化后就不会再被初始化,保证了单例。
3.static类型的instance存在静态存储区,每次调用时,都指向的同一个对象。
■ 2.2 C++ 实现单例的几种方式
- 懒汉式单例
1> 普通懒汉式单例
2> 加锁懒汉式单例
3> 内部静态变量的懒汉式单例 - 饿汉式单例
● 2.2.1 普通懒汉式单例
懒汉式:直到使用时才实例化对象,直到调用getInstance() 方法的时候才 new 一个单例的对象。优点是如果不被调用就不会占用内存。
#include <iostream>
using namespace std;
class LazySingleton
{
private:
LazySingleton()
{
cout << "This is constructor!" << endl;
}
LazySingleton(LazySingleton&)=delete;
LazySingleton& operator=(const LazySingleton&) = delete;
static LazySingleton* m_pInstance;
public:
~LazySingleton()
{
cout << "This is destructor!" << endl;
}
static LazySingleton* getInstance()
{
if(!m_pInstance)
m_pInstance = new LazySingleton;
return m_pInstance;
}
};
LazySingleton* LazySingleton::m_pInstance = nullptr;
int main()
{
LazySingleton* pInstance = LazySingleton::getInstance();
LazySingleton* pInstance_2 = LazySingleton::getInstance();
return 0;
}
运行结果:
This is constructor!
根据以上程序结果可知,获取了两次类的实例,却只有一次构造函数被调用,表明只生成了唯一实例,这是最基础版本的单例实现,他有哪些问题呢?
- 线程安全:
- 当多线程获取单例时有可能引发竞态条件:第一个线程在if中判断m_pInstance 是空的,于是开始实例化单例;同时第2个线程也尝试获取单例,这个时候判断 m_pInstance 还是空的,于是也开始实例化单例;这样就会实例化出两个对象,这就是线程安全问题的由来; 解决办法: 加锁
- 内存泄漏:
- 该单例类只负责new出对象,却没有delete对象,所以只调用了构造函数,析构函数却没有被调用,因此会导致内存泄漏。解决办法: 使用智能指针(shared_ptr)
● 2.2.2 线程安全、内存安全的懒汉式单例 (智能指针和线程锁)
#include <iostream>
#include <memory> // shared_ptr
#include <mutex> // mutex
using namespace std;
class LazySingleton
{
public:
typedef shared_ptr<LazySingleton> Ptr;
~LazySingleton()
{
cout << "This is destructor!" << endl;
}
LazySingleton(LazySingleton&)=delete;
LazySingleton& operator=(const LazySingleton&) = delete;
static Ptr getInstance()
{
if(m_pInstance == nullptr) // 双检索检测
{
std::lock_guard<std::mutex> lk(m_mutex);
if(m_pInstance == nullptr)
{
m_pInstance = shared_ptr<LazySingleton>(new LazySingleton);
}
}
return m_pInstance;
}
private:
LazySingleton()
{
cout << "This is constructor!" << endl;
}
static Ptr m_pInstance;
static mutex m_mutex;
};
// initialization static variables out of class
LazySingleton::Ptr LazySingleton::m_pInstance = nullptr;
mutex LazySingleton::m_mutex;
int main()
{
LazySingleton::Ptr instance = LazySingleton::getInstance();
LazySingleton::Ptr instance2 = LazySingleton::getInstance();
return 0;
}
运行结果如下,只调用了一次构造函数,并且发生了析构。
This is constructor!
This is destructor!
以上方法的优点:
基于 shared_ptr, 用C++比较倡导的 RAII思想,用对象管理资源,当 shared_ptr 析构的时候,new 出来的对象也会被 delete掉,避免内存泄漏。
- 加锁以后,使用互斥量来达到线程安全。这里使用了两个 if判断语句的技术称为双检锁;优点是只有判断指针为空的时候才加锁,避免每次调用 getInstance的方法都加锁,减少加锁的次数,从而达到减小开销的目的。
不足之处:
- 使用线程锁开销很大
- 代码量大
- 双检锁失效(部分平台)
因此这里提供第三种方法达到线程安全
● 2.2.3 Meyers单例:局部静态变量(最推荐的懒汉式)
#include <iostream>
using namespace std;
class MeyerSingleton
{
public:
~MeyerSingleton()
{
cout << "This is destructor!" << endl;
}
MeyerSingleton(const MeyerSingleton&) = delete;
MeyerSingleton& operator=(const MeyerSingleton&) = delete;
static MeyerSingleton& getInstance()
{
static MeyerSingleton instance;
return instance;
}
private:
MeyerSingleton()
{
cout << "This is constructor!" << endl;
}
};
int main()
{
MeyerSingleton& instance_1 = MeyerSingleton::getInstance();
MeyerSingleton& instance_2 = MeyerSingleton::getInstance();
return 0;
}
运行结果
This is constructor!
This is destructor!
如果当变量在初始化的时候,并发同时进入声明语句,并发线程将会阻塞等待初始化结束。
这样保证了并发线程在获取静态局部变量的时候一定是初始化过的,具有线程安全性。
C++静态变量的生存期 是从声明到程序结束,也是一种懒汉式单例。以上是各种方法实现单例的代码和说明,这里推荐 Meyer单例的方式,使用静态局部变量的方法,因为其几乎能满足所有单例所需的要求。
- 通过局部静态变量的特性保证了线程安全;
- 不需要使用共享指针;
注意在使用的时候需要声明单例的引用 MeyerSingleton& 才能获取对象。
更多设计模式,请关注: