先简单介绍一下单例模式:
单例模式(Singletion Pattern)是一种软件开发中的设计模式,属于创建型模式(也称工厂模式,封装对象的创建过程,使客户端可以透明地创建对象,而不需要关心对象的内部实现细节)。单例模式确保一个类只有一个实例,并提供一个全局访问点来访问这个实例。单例模式通常用于管理共享资源,如数据库连接、 文件系统、硬件设备等,或者在多个线程之间共享数据。
由上面的定义,我们也可以知道单例模式的几个特点:
- 全局唯一性:确保一个类只有一个实例,并提供一个全局访问点。
- 自我实例化:单例类负责创建自己的唯一实例。
- 延迟初始化:单例实例通常在第一次使用时创建,而不是在程序启动时。
- 可访问性:单例类通常提供一个静态的访问方法,允许客户端访问其唯一实例。
实现单例模式需要以下几个步骤:
- **私有构造函数:**防止外部通过new直接创建类的实例。
- 静态私有实例变量:保存类的唯一实例。
- **静态公有方法:**提供一个全局访问点用于获取唯一实例。
简单的单例模式(懒汉Lazy Singleton)
cpp
#include <iostream>
class Singleton{
private:
//私有构造函数
Singleton(){}
//静态私有实例变量
static Singleton* instance;
public:
//禁止拷贝构造函数和赋值操作
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
//公有静态方法,提取全局访问点
static Singleton* getInstance(){
//判断是否第一次调用
if(instance = nullptr){
instance = new Singleton();
}
return instance;
}
//实例方法
void dosomething(){
std::cout<<"Hello world"<<std:;endl;
}
}
// 初始化静态私有实例变量
Singleton * Singleton::instance = nullptr;
int main(){
//获取单例实例
Singleton* singleton = Singleton::getInstance();
//使用单例实例
singleton->dosomething();
}
这个简单实例也称为懒汉单例模式(Lazy Singleton),单例在第一次使用才会被初始化,或称为延迟初始化,也正因为仅在第一次使用时创建单例实例,能够节省资源,但在某些情况下,可能会导致内存泄漏。举例如下:
- 单例实例通常为静态成员变量,静态成员变量的生命周期与程序的生命周期相同,若单例实例持有资源(动态分配的内存、文件描述符等),而这些资源在程序运行期间未正确释放,会导致内存泄漏。
- 单例实例是静态的,它的销毁通常需要在程序结束时通过显式的代码来完成。如果忘记编写这样的销毁代码,持有的资源不会释放。
这里提供一个会造成内存泄漏的例子
cpp
class Singleton {
private:
static Singleton* instance;
// 假设单例持有动态分配的内存
int* data;
Singleton() {
data = new int[100]; // 动态分配内存
}
public:
static Singleton* getInstance() {
if (instance == nullptr) {
instance = new Singleton();
}
return instance;
}
// 公有析构函数,虽然是公有的,但由于是私有的构造函数,外部无法创建实例
~Singleton() {
delete[] data; // 释放动态分配的内存
}
};
Singleton* Singleton::instance = nullptr;
int main() {
// 使用单例
Singleton* singleton = Singleton::getInstance();
// 单例实例持有的动态分配的内存没有释放
return 0;
}
Singleton持有一个指向动态分配数组的指针data,由于没有提供销毁单例实例的函数,当程序结束时,data指向的内存未被释放,从而导致内存泄漏。
针对这一问题,有以下两种解决方案:
1.使用智能指针
2.使用静态的嵌套类对象
智能指针
上述代码可以改为:
cpp
#include <memory> // 包含智能指针的头文件
class Singleton {
private:
static std::unique_ptr<Singleton> instance; // 使用智能指针管理单例实例
int* data;
// 私有构造函数
Singleton() {
data = new int[100]; // 动态分配内存
}
// 禁止拷贝构造函数和赋值操作符
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
public:
// 静态公有方法,提供全局访问点
static Singleton* getInstance() {
if (instance.get() == nullptr) {
instance.reset(new Singleton()); // 使用智能指针管理单例实例
}
return instance.get();
}
// 公有析构函数,虽然是公有的,但由于是私有的构造函数,外部无法创建实例
~Singleton() {
delete[] data; // 释放动态分配的内存
}
};
// 初始化静态成员变量
std::unique_ptr<Singleton> Singleton::instance = nullptr;
int main() {
// 使用单例
Singleton* singleton = Singleton::getInstance();
// 程序结束时,智能指针会自动释放单例实例,包括其持有的动态分配的内存
return 0;
}
嵌套类
cpp
class Singleton {
private:
static Singleton* instance; // 静态指针,用于存储单例的唯一实例
int* data; // 指向动态分配内存的指针
// 私有构造函数,只能内部调用
Singleton() {
data = new int[100]; // 动态分配内存
}
// 私有析构函数,只能内部调用
~Singleton() {
delete[] data; // 释放动态分配的内存,防止内存泄漏
}
// 内部类,用于在程序结束时自动销毁单例实例
class Deletor {
public:
// 析构函数,在程序结束时调用
~Deletor() {
if (Singleton::instance != nullptr)
delete Singleton::instance; // 如果单例实例存在,则删除它
}
};
// 静态成员变量,用于确保Deletor的析构函数在程序结束时被调用
static Deletor deletor;
public:
// 静态公有方法,提供全局访问点,用于获取单例实例
static Singleton* getInstance() {
if (instance == nullptr) { // 如果单例实例尚未创建
instance = new Singleton();
}
return instance;
}
};
// 初始化静态成员变量
Singleton* Singleton::instance = nullptr; // 初始化为nullptr,表示单例实例尚未创建
Singleton::Deletor Singleton::deletor; // 创建Deletor实例,以便在程序结束时调用其析构函数
// 主函数
int main() {
Singleton* singleton = Singleton::getInstance(); // 获取单例实例
// 使用单例实例...
return 0; // 程序结束,Deletor的析构函数将被调用,自动销毁单例实例
}
在程序运行结束时,系统会调用静态成员deletor的析构函数,该析构函数会删除单例的唯一实例。用这种方法释放单例对象。
- 在单例类内部定义专有的嵌套类。
- 在单例类内定义私有的专门用于释放的静态成员
- 利用程序在结束时析构全局变量的特性,选择最终的释放时机。
对于内存泄漏的检测,考虑使用valgrind
多线程下的单例模式
这些代码在单线程下环境是正确的,但是当处于多线程环境时,会发生竞争。最简单的解决竞争的方式是加入锁机制来保护临界区。
于是就有了最简单的多线程环境下的单例模式代码:
cpp
#include <iostream>
class Singleton{
private:
//私有构造函数
Singleton(){}
//静态私有实例变量
static Singleton* instance;
mutex mutex;
public:
//禁止拷贝构造函数和赋值操作
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
//公有静态方法,提取全局访问点
static Singleton* getInstance(){
//判断是否第一次调用
//上锁
mutex.lock();
if(instance = nullptr){
instance = new Singleton();
}
//解锁
mutex.unlock();
return instance;
}
//实例方法
void dosomething(){
std::cout<<"Hello world"<<std:;endl;
}
}
// 初始化静态私有实例变量
Singleton * Singleton::instance = nullptr;
int main(){
//获取单例实例
Singleton* singleton = Singleton::getInstance();
//使用单例实例
singleton->dosomething();
}
Double Checked Locking(DCL)
考虑到线程安全仅存在于第一次初始化(new)过程中,而在后续获取该实例时并不会遇到,也就没有必要再使用lock。使用双重检查锁定(Double-Checked Locking)来确保单例实例的线程安全,主要目的时减少加锁的次数。在细节上:
- 第一次检查:在getinstance方法中,首先检查instance是否为nullptr,若是,则继续执行、
- 加锁:使用std::lock_guard来确保mutex在作用域内被正确地加锁和解锁。这种方法被称为作用域锁 ,因为它在lock_guard对象的作用域结束时自动解锁。
- 第二次检查:在加锁前,再次检查instance是否为nullptr,若仍为nullptr,则加锁并创建单例实例。
- 创建实例:在加锁后,如果instance仍为nullptr,则创建实例。
- 返回实例:无论是否创建了新实例,最终都返回instance指针。
cpp
static Singleton* getInstance() {
if (instance == nullptr) {
std::lock_guard<std::mutex> lock(mutex); // 作用域锁
if (instance == nullptr) {
instance = new Singleton(); // 创建单例实例
}
}
return instance;
}
"加入DCL后,其实还是有问题的,关于memory model。在某些内存模型中(虽然不常见)或者是由于编译器的优化以及运行时优化等等原因,使得instance虽然已经不是nullptr但是其所指对象还没有完成构造,这种情况下,另一个线程如果调用getInstance()就有可能使用到一个不完全初始化的对象。换句话说,就是代码中第2行:if(instance == NULL)和第六行instance = new Singleton();没有正确的同步,在某种情况下会出现new返回了地址赋值给instance变量而Singleton此时还没有构造完全,当另一个线程随后运行到第2行时将不会进入if从而返回了不完全的实例对象给用户使用,造成了严重的错误。在C++11没有出来的时候,只能靠插入两个memory barrier(内存屏障)来解决这个错误,但是C++11引进了memory model,提供了Atomic实现内存的同步访问,即不同线程总是获取对象修改前或修改后的值,无法在对象修改期间获得该对象。"
C++ 单例模式 - 知乎 (zhihu.com)
C++11规定了local static在多线程条件下的初始化行为,要求编译器保证了内部静态变量的线程安全性。在C++11标准下,《Effective C++》提出了一种更优雅的单例模式实现,使用函数内的 local static 对象。这样,只有当第一次访问getInstance()
方法时才创建实例。这种方法也被称为Meyers' Singleton。C++0x之后该实现是线程安全的,C++0x之前仍需加锁。
Meyers' Singleton(也称为Singleton模式或懒汉式Singleton模式)是一种单例模式实现,它确保一个类只有一个实例,并提供一个全局访问点来获取这个实例。与传统的懒汉式单例模式不同,Meyers' Singleton在C++中通过使用内部静态类来实现,这是一种在C++11之前常用的方法,因为它能够保证线程安全,同时避免在全局范围内声明静态对象。
Meyers' Singleton的关键点在于它使用了内部静态类来延迟实例的创建,直到首次使用时才创建实例。内部静态类在类定义时创建,因此它不会被全局实例化,从而避免了全局初始化的开销。
cpp
class Singleton {
public:
static Singleton& getInstance() {
// 内部静态类,确保在类定义时创建,而不是在全局范围内实例化
static Singleton instance;
return instance;
}
private:
// 私有构造函数,防止外部通过new创建对象实例
Singleton() {};
// 私有拷贝构造函数,防止外部复制对象实例
Singleton(const Singleton&) = delete;
// 私有拷贝赋值运算符,防止外部复制对象实例
Singleton& operator=(const Singleton&) = delete;
};
getInstance
方法返回一个内部静态类Singleton
的实例。由于Singleton
是一个内部静态类,它不会在全局范围内实例化,而是在类定义时实例化。这意味着只有在首次调用getInstance
方法时,才会创建单例实例。
Meyers' Singleton模式提供了一种线程安全的懒汉式单例实现,它避免了全局初始化的开销,并且通过内部静态类保证了线程安全。在C++11之后,可以使用std::call_once
或std::once_flag
来更简洁地实现单例模式。
饿汉式单例模式
单例实例在程序运行时被立即执行初始化,它确保一个类只有一个实例,并且在类加载时立即创建这个实例。饿汉式单例模式的特点是实例在类加载时就创建,因此不需要在第一次使用时创建,这使得饿汉式单例模式在类加载时就会占用一定的内存。
饿汉式单例模式的优点是简单且线程安全,因为实例在类加载时就已经创建,所以不会出现线程竞争的问题。然而,它的缺点是资源利用率不高,因为实例在类加载时就创建了,即使你从未使用过这个单例。
cpp
class Singleton {
private:
// 私有构造函数,防止外部通过new创建对象实例
Singleton() {}
// 私有拷贝构造函数,防止外部复制对象实例
Singleton(const Singleton&) = delete;
// 私有拷贝赋值运算符,防止外部复制对象实例
Singleton& operator=(const Singleton&) = delete;
public:
// 静态成员变量,在程序启动时创建单例实例
static const Singleton& getInstance() {
return instance;
}
private:
// 静态成员变量,类加载时初始化
static const Singleton instance;
};
// 在类外初始化静态成员变量
const Singleton Singleton::instance;
-
静态常量成员变量 :
instance
是一个静态常量成员变量,它在程序启动时创建,并且在整个程序运行期间都存在。 -
构造函数和析构函数 :由于
instance
是一个常量,因此必须在类外部进行初始化,并且只能通过常量表达式进行初始化。 -
线程安全:饿汉式单例模式在程序启动时就创建了单例实例,因此它本身就是线程安全的,不需要担心多线程下的初始化问题。
-
常量引用返回 :
getInstance
方法返回的是对单例实例的常量引用,这防止了对单例实例的修改。 -
不需要显式析构函数 :由于
instance
是静态常量成员,它的生命周期由系统管理,不需要显式析构。
参考文章: