一句话描述
确保一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。
理解
-
“确保一个类只有一个实例”
- 这点就要求其构造方法不能是public公开的,一定是不能随便被外面所调用,即不能被外界进行实例化,则它的构造方法只能是private。
-
“而且自行实例化并向整个系统提供这个实例”
-
只有一个实例的情况下,向整个系统提供这个实例,那么这个实例是必属于这个类的,所以这个实例是类成员变量,即一定是静态变量。
-
而且,要向整个系统提供这个实例,那么只能是一个
静态成员方法
。
C++中,静态变量只能通过静态成员方法访问。
-
作用
显而易见,确保一个类只有一个实例存在,比如序号生成、计数器、全局的管理类,在软件架构中应用广泛。
两种实现
根据单例类对象实例化时间段,对单例类的实现,可以分为两类。
饿汉式
#include <iostream>
class Only {
public:
// 提供类实例的唯一接口,静态成员方法
static Only* GetInstance() {
return m_singleton;
}
private:
// 类构造函数一定是private修饰
Only () {
};
// 类实例对象一定是一个静态变量
static Only* m_singleton;
}
// 静态成员变量(属性)类外初始化,注意需要在cpp文件初始化
Only* Only::m_singleton = new Only();
显而易见,饿汉式的实现在程序加载时就完成,比较着急,所以称为饿汉式。
懒汉式
#include <iostream>
class Only {
public:
// 提供类实例的唯一接口,静态成员方法
static Only* GetInstance() {
if(NULL == m_singleton) {
m_singleton = new Only();
}
return m_singleton;
}
private:
// 类构造函数一定是private修饰
Only () {
};
// 类实例对象一定是一个静态变量
static Only* m_singleton;
}
// 静态成员变量(属性)类外初始化
Only* Only::m_singleton = NULL;
显而易见,懒汉式的实例初始化在第一次调用获得类实例接口时,不调用则不初始化,比较懒。
双重检查锁
由懒汉式的实现原理可以料想到,在多线程的情况下,可能会遇到static Only* GetInstance()
方法,同时被多个线程调用的情况,这样就会static Only* singleton
就会被初始化多次,从而发生内存泄漏,那怎么办呢?加锁。
#include <iostream>
#include <mutex>
// 锁
mutex m_Only_mutex;
class Only {
public:
// 提供类实例的唯一接口,静态成员方法
static Only* GetInstance() {
m_Only_mutex.lock();
if(NULL == m_singleton) {
m_singleton = new Only();
}
m_Only_mutex.unlock();
return m_singleton;
}
private:
// 类构造函数一定是private修饰
Only () {
};
// 类实例对象一定是一个静态变量
static Only* m_singleton;
}
// 静态成员变量(属性)类外初始化
Only* Only::m_singleton = NULL;
貌似这样就可以了,但是,其实有两个可以优化的地方:
-
锁范围可以进一步缩小,毕竟
static Only* GetInstance()
,这个接口可能会被调用多次,所以应该判空语句内部加锁,那么可能同时多个线程运行到判空语句,所以在互斥区的第一句中还需要判空检查一次,这就是双重检查锁。 -
在多线程使用时,对于当前的单例类实例对象应该加volatile修饰,为什么呢?因为
m_singleton = new Only();
可以拆分为3步:
- 分配内存
- 初始化对象
- 将
m_singleton
指向刚分配的内存
这个操作过程其实是非原子操作的,也就是说,在这过程中,CPU可能会发生重排序的情况。比如:线程A已经执行了步骤1和步骤3,还未执行步骤2,这时候
m_singleton
已经不为空了,假如线程B前,调用static Only* GetInstance()
那么B将得到一个没有初始化,但有值的m_singleton
,所以,应该使用volatile修饰,避免重排序。
#include <iostream>
#include <mutex>
// 锁
mutex m_Only_mutex;
class Only {
// 提供类实例的唯一接口,静态成员方法
static Only* GetInstance() {
// 双重检查锁
if(NULL == m_singleton) {
m_Only_mutex.lock();
if(NULL == m_singleton) {
m_singleton = new Only();
}
}
m_Only_mutex.unlock();
return m_singleton;
}
private:
// 类构造函数一定是private修饰
Only () {
};
// 类实例对象一定是一个静态变量
static Only* volatile m_singleton;
}
// 静态成员变量(属性)类外初始化
Only* Only::m_singleton = NULL;
内存回收优化
对于单例类的实例来讲,有时候必须要将当前实例释放掉,比如关闭文件,释放外部资源。但由于实例是静态成员变量,在类的析构行为中无法实现这个要求。我们需要一种方法,正常地删除该实例。
-
可以在程序结束时调用
GetInstance()
并对返回的指针调用delete操作。
这样做可以实现功能,不是很优美,而且容易出错。因为这样的附加代码很容易被忘记,而且也很难保证在delete之后,没有代码再调用
GetInstance()
(针对于饿汉式)。 -
让这个类自己知道在合适的时候把自己删除。或者说把删除自己的操作挂在系统中的某个合适的点上,使其在恰当的时候自动被执行。
程序在结束的时候,系统会自动析构所有的全局变量。事实上,系统也会析构所有的类的静态成员变量,就像这些静态成员也是全局变量一样。所以,利用这个特性,实现单例类实例,在程序结束时自动释放资源。
#include <iostream>
#include <mutex>
// 锁
mutex m_Only_mutex;
class Only {
// 提供类实例的唯一接口,静态成员方法
static Only* GetInstance() {
// 双重检查锁
if(NULL == m_singleton) {
m_Only_mutex.lock();
if(NULL == m_singleton) {
m_singleton = new Only();
}
}
m_Only_mutex.unlock();
return m_singleton;
}
private:
class AutoRelease {
public:
AutoRelease() {
}
// 通过AutoRelease类的析构函数,释放m_singleton
~AutoRelease() {
if(m_singleton) {
delete m_singleton;
}
}
};
// 类构造函数一定是private修饰
Only () {
};
// 类实例对象一定是一个静态变量
static Only* volatile m_singleton;
// 类静态变量,用于程序退出时,单例类自动析构
static AutoRelease _autoRelease;
}
// 静态成员变量(属性)类外初始化
Only* Only::m_singleton = NULL;
Only::AutoRelease Only::_autoRelease;
参考目录
- https://www.bilibili.com/video/BV1af4y1y7sS?spm_id_from=333.999.0.0
- https://blog.csdn.net/qq_29542611/article/details/79301595
- https://blog.csdn.net/weixin_44363885/article/details/92838607
- https://bbs.csdn.net/topics/600461189
- 单例模式内存释放