一、概念
单例模式(Singleton Pattern)是一种创建型设计模式,确保一个类只有一个实例,并提供全局访问点。在C++中,实现单例模式需要考虑到线程安全、延迟初始化以及避免全局对象初始化顺序问题等因素。
二、主要思想
单例模式的主要思想是控制类实例的数量并集中管理访问。它通过一个全局访问点来提供类的唯一实例,从而避免因为多次实例化导致的资源浪费或不一致性。这通常是通过私有化构造函数来实现的,以阻止外部通过new
直接创建对象实例。相反,类本身负责创建自己的唯一实例,并确保所有外部对该实例的请求都返回同一个对象引用。
三、优缺点
优点:
- 在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例时。
- 避免对资源的多重占用,可以方便地存储和控制状态信息。
- 提供了一个全局访问点,使得整个应用程序可以通过这一点访问单例对象,有助于更好地控制资源。
缺点:
- 过度依赖单例模式可能使代码变得紧耦合和难以测试。
- 单例模式限制了类的实例化次数,这在一定程度上增加了扩展的难度。
- 如果单例类管理一些资源(如打开的文件、网络连接等),在析构函数中释放这些资源是一个合理的做法,但这也可能导致资源释放的顺序问题。
四、单例模式的分类及区别
4.1 单例模式的分类
在C++中,单例模式可以根据其实现方式和特性分为几种不同的类型,主要包括:
4.1.1 饿汉式
- 在类加载时就完成了实例的初始化,因此是线程安全的。
- 但无法实现延迟加载,可能造成资源浪费。
- 应用场景:适用于在程序启动时就需要创建实例,且实例的创建开销不大,或者实例的创建不依赖于其他资源的情况。
- 优点:实现简单,线程安全,因为实例在类加载时已经创建。
- 缺点:无法实现延迟加载,如果实例在程序执行过程中并不需要,则会造成资源浪费。
#include <iostream>
#include <mutex>
class Singleton {
public:
// 禁止拷贝构造函数和赋值操作符
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
// 提供一个全局访问点来获取单例实例
static Singleton& getInstance() {
// 由于静态局部变量在第一次使用时初始化,并且只初始化一次,
// 所以这里利用这个特性来创建单例实例。
static Singleton instance;
return instance;
}
// 其他成员函数
void doSomething() {
std::cout << "Doing something in the Singleton instance." << std::endl;
}
private:
// 私有构造函数,防止外部直接创建实例
Singleton() {
// 可以在这里进行资源初始化等操作
std::cout << "Singleton instance created." << std::endl;
}
// 私有析构函数,防止外部删除实例(虽然在这个例子中由于静态局部变量的特性,析构函数不会被外部调用)
~Singleton() {
std::cout << "Singleton instance destroyed." << std::endl;
}
};
int main() {
// 获取单例实例并调用其成员函数
Singleton& singleton = Singleton::getInstance();
singleton.doSomething();
// 注意:由于实例是静态局部变量,在main函数结束时它会被自动销毁。
// 但是,由于C++的静态存储期规则,这个销毁过程并不会显式地体现在代码中(即不会有显式的delete调用)。
return 0;
}
在这个示例中,Singleton
类有一个私有的构造函数和一个私有的析构函数,以防止外部直接创建或删除实例。getInstance
方法提供了一个全局访问点来获取单例实例。由于getInstance
方法中的静态局部变量instance
在第一次调用时初始化,并且只初始化一次,因此它实现了单例模式。
需要注意的是,虽然这个示例在功能上类似于饿汉式单例,但严格来说,它并不是在类加载时立即创建实例(因为C++没有类加载的概念)。然而,由于静态局部变量的初始化特性,这个实现方式在效果上非常接近饿汉式单例,即实例在程序运行到getInstance
方法时就已经存在,且只存在一个实例。
另外,由于C++11引入了线程安全的静态局部变量初始化,因此这个实现在多线程环境下也是安全的。
4.1.2 懒汉式
- 在第一次调用时实例化,实现了延迟加载。
- 但需要处理线程安全问题,如果处理不当,可能会导致多个实例被创建。
- 应用场景:适用于在程序运行过程中按需创建实例,且实例的创建开销较大,或者实例的创建依赖于其他资源的情况。
- 优点:可以实现延迟加载,节省资源。
- 缺点:需要处理线程安全问题,如果多个线程同时调用
getInstance
方法,可能会导致多个实例被创建(非线程安全的懒汉式)。
以下是一个线程不安全的懒汉式单例模式的C++示例(仅用于教学目的,不推荐在实际使用):
#include <iostream>
class Singleton {
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 << "Doing something in the Singleton instance." << std::endl;
}
// 提供一个清理函数(在实际应用中,通常不需要手动调用,除非有特殊的资源管理需求)
static void cleanup() {
delete instance_;
instance_ = nullptr;
}
private:
// 私有构造函数,防止外部直接创建实例
Singleton() {
std::cout << "Singleton instance created." << std::endl;
}
// 私有析构函数,防止外部删除实例(在这个例子中,我们提供了一个cleanup函数来管理资源)
~Singleton() {
std::cout << "Singleton instance destroyed." << std::endl;
}
// 静态指针,用于存储单例实例
static Singleton* instance_;
};
// 初始化静态指针为nullptr
Singleton* Singleton::instance_ = nullptr;
int main() {
// 获取单例实例并调用其成员函数
Singleton* singleton = Singleton::getInstance();
singleton->doSomething();
// 注意:在这个例子中,我们没有手动调用cleanup函数,因为程序结束时会自动销毁全局/静态对象。
// 然而,在实际应用中,如果单例持有需要手动释放的资源(如文件句柄、网络连接等),
// 则应该提供一个清理机制(如cleanup函数)来确保资源被正确释放。
// 另外,由于这个示例是线程不安全的,因此在实际多线程环境中使用时需要额外的同步机制。
return 0;
}
线程安全的懒汉式单例
为了使懒汉式单例在多线程环境中安全,我们需要添加同步机制。以下是一个使用互斥锁(std::mutex
)来实现线程安全的懒汉式单例的示例:
#include <iostream>
#include <mutex>
class Singleton {
public:
// ...(与上面相同)
// 线程安全的getInstance方法
static Singleton* getInstance() {
std::lock_guard<std::mutex> lock(mutex_); // 使用RAII风格的锁来管理互斥锁的生命周期
if (instance_ == nullptr) {
instance_ = new Singleton();
}
return instance_;
}
// ...(与上面相同)
private:
// ...(与上面相同)
// 静态互斥锁,用于保护单例实例的创建过程
static std::mutex mutex_;
// 静态指针,用于存储单例实例
static Singleton* instance_;
};
// 初始化静态成员变量
std::mutex Singleton::mutex_;
Singleton* Singleton::instance_ = nullptr;
// ...(main函数与上面相同)
在这个线程安全的版本中,我们添加了一个静态的std::mutex
成员变量mutex_
,并在getInstance
方法中使用std::lock_guard
来管理锁的生命周期。这样,即使多个线程同时调用getInstance
方法,也只有一个线程能够创建实例,从而保证了单例性。
4.1.3 双重检查锁定(DCL):
- 结合了懒汉式和同步锁的优点,既实现了延迟加载,又保证了线程安全。
- 但实现起来相对复杂,需要谨慎处理。
- 应用场景:适用于高并发环境,需要确保线程安全且希望减少同步锁开销的情况。
- 优点:结合了懒汉式和同步锁的优点,既实现了延迟加载,又减少了同步锁的开销。
- 缺点:实现复杂,需要谨慎处理以避免潜在的问题,如“伪共享”等。
双重检查锁定(Double-Checked Locking)是一种优化技术,用于在多线程环境中实现懒汉式单例模式,同时减少获取锁的开销。在C++11及更高版本中,我们可以利用std::atomic
和std::mutex
来实现线程安全的双重检查锁定。
#include <iostream>
#include <atomic>
#include <mutex>
class Singleton {
public:
// 禁止拷贝构造函数和赋值操作符
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
// 提供一个全局访问点来获取单例实例
static Singleton* getInstance() {
if (instance_ == nullptr) { // 第一次检查,无需加锁
std::lock_guard<std::mutex> lock(mutex_); // 加锁进行第二次检查
if (instance_ == nullptr) { // 第二次检查,确保实例未被其他线程创建
instance_ = new Singleton();
}
}
return instance_;
}
// 其他成员函数
void doSomething() {
std::cout << "Doing something in the Singleton instance." << std::endl;
}
// 提供一个清理函数(通常不需要手动调用,除非有特殊需求)
static void cleanup() {
delete instance_;
instance_ = nullptr;
}
private:
// 私有构造函数,防止外部直接创建实例
Singleton() {
std::cout << "Singleton instance created." << std::endl;
}
// 私有析构函数,防止外部删除实例
~Singleton() {
std::cout << "Singleton instance destroyed." << std::endl;
}
// 静态原子指针,用于存储单例实例,保证线程安全
static std::atomic<Singleton*> instance_;
// 静态互斥锁,用于保护单例实例的创建过程
static std::mutex mutex_;
};
// 初始化静态成员变量
std::atomic<Singleton*> Singleton::instance_(nullptr);
std::mutex Singleton::mutex_;
int main() {
// 获取单例实例并调用其成员函数
Singleton* singleton = Singleton::getInstance();
singleton->doSomething();
// 注意:cleanup函数通常不需要手动调用,因为程序结束时会自动销毁全局/静态对象。
// 然而,如果单例持有需要手动释放的资源(如文件句柄、网络连接等),
// 则应该提供一个清理机制(如cleanup函数)来确保资源被正确释放。
// 在这个例子中,我们没有手动调用cleanup函数。
return 0;
}
在这个示例中,instance_
是一个std::atomic<Singleton*>
类型的静态成员变量,它保证了指针赋值的原子性。第一次检查instance_ == nullptr
时不需要加锁,这减少了获取锁的开销。如果第一次检查发现实例尚未创建,则进入加锁区域进行第二次检查,以确保实例未被其他线程创建。如果实例确实尚未创建,则创建它。
需要注意的是,虽然双重检查锁定可以减少获取锁的开销,但它也增加了代码的复杂性。此外,C++11及更高版本中的std::call_once
和std::once_flag
提供了一种更简单且线程安全的方式来初始化静态局部变量,这可能是实现单例模式的更好选择。然而,了解双重检查锁定的实现原理仍然是有价值的,因为它展示了如何在多线程环境中进行精细的同步控制。
4.1.4 静态内部类(C++11及以后):
- 利用C++11的局部静态变量初始化特性,可以实现既线程安全又延迟加载的单例模式。
- 这种方法实现简单且高效,是C++11及以后版本的推荐实现方式。
- 应用场景:适用于C++11及以后版本,希望实现线程安全且简洁的单例模式。
- 优点:利用C++11的局部静态变量初始化特性,既线程安全又实现了延迟加载,且实现简洁。
- 缺点:需要C++11及以上版本的支持。
4.1.5 使用std::once_flag
和std::call_once
(C++11及以上):
- 确保某个代码块只被执行一次,从而实现单例模式的线程安全和延迟加载。
- 依赖于C++11及以上版本的特性。
- 应用场景:适用于C++11及以上版本,希望确保某个代码块只被执行一次的情况。
- 优点:利用C++11的标准库特性,确保线程安全且易于理解。
- 缺点:需要C++11及以上版本的支持。
4.1.6 枚举单例:
- 应用场景:适用于C++11及以上版本,希望防止外部通过
delete
操作符删除单例实例的情况。 - 优点:利用枚举类型的特性,确保单例实例在全局范围内唯一且不可被删除。
- 缺点:需要C++11及以上版本的支持,且实现方式相对不常见。
4.1.7 线程局部存储:
- 应用场景:在多线程环境下,每个线程都需要一个独立的实例,但这些实例之间又需要保持某种独立性(不是全局单例)。
- 优点:为每个线程提供独立的实例,避免了线程间的数据竞争。
- 缺点:虽然严格意义上不是单例模式,但在某些多线程场景下有其独特的用途。
4.2 单例模式分类的区别
- 饿汉式:
- 在类加载时就完成了实例的初始化。
- 线程安全,因为实例在类加载时已经创建,无需担心多线程访问问题。
- 无法实现延迟加载,如果实例在程序执行过程中并不需要,则会造成资源浪费。
- 懒汉式:
- 在第一次调用
getInstance
方法时才创建实例。 - 可以实现延迟加载,节省资源。
- 需要处理线程安全问题,如果多个线程同时调用
getInstance
方法,可能会导致多个实例被创建。
- 在第一次调用
- 双重检查锁定(DCL):
- 结合了懒汉式和同步锁的优点。
- 使用双重检查来减少同步锁的开销,并确保线程安全。
- 实现相对复杂,需要谨慎处理以避免潜在的问题。
- 静态内部类(Meyers' Singleton):
- 利用C++11的局部静态变量初始化特性。
- 既线程安全又实现了延迟加载。
- 实现简单且高效,是C++11及以后版本的推荐实现方式。
- 使用
std::call_once
和std::once_flag
:- 依赖于C++11及以上版本的特性。
- 确保某个代码块只被执行一次,从而实现单例模式的线程安全和延迟加载。
- 相比双重检查锁定,这种方式更加简洁且易于理解。
标签:std,Singleton,模式,实例,线程,C++,单例,设计模式 From: https://blog.csdn.net/a8039974/article/details/143804756