目录
2.2. 饿汉式(Eager Initialization)
2.3. 双重检查锁定(Double-Checked Locking)
2.4. 静态内部类(Static Inner Class)
单例模式(Singleton Pattern)是一种常用的软件设计模式,它的主要目的是确保一个类仅有一个实例,并提供一个全局访问点来获取该实例。这种模式特别适用于管理资源、控制对共享资源的访问,或者创建一些中心控制类,如日志记录器、数据库连接池等。
一、单例模式的特点
单例模式(Singleton Pattern)的特点可以总结为以下几点:
- 全局唯一性:单例类保证在整个应用程序的生命周期中只有一个实例被创建。这是单例模式的核心特点,它避免了因多次创建实例而导致的资源消耗和数据不一致问题。
- 提供全局访问点:单例类提供一个全局的访问点(通常是静态方法),允许应用程序的任何部分在需要时访问这个唯一的实例。这个访问点通常封装了实例的创建逻辑,确保实例的唯一性。
- 自行实例化:单例类负责创建并管理自己的实例。这意味着实例的创建和销毁(尽管在单例模式的典型应用中,实例通常不会被销毁)都是由类本身控制的。
- 延迟实例化(可选):虽然单例模式的实现可以立即在类加载时创建实例(称为“饿汉式”),但更常见的做法是延迟实例化,即在第一次需要时才创建实例(称为“懒汉式”或“延迟加载”)。这有助于减少应用程序启动时的开销和内存占用。
- 线程安全(在多线程环境下):在多线程环境中,单例模式的实现必须确保线程安全,以防止多个线程同时创建实例。这通常通过使用同步机制(如
synchronized
关键字、ReentrantLock
等)或利用类加载机制(如静态内部类方式)来实现。 - 控制实例的创建和销毁(在需要时):尽管单例模式的实例通常与应用程序的生命周期相同,但在某些情况下,可能需要更精细地控制实例的创建和销毁时机。然而,这通常超出了单例模式本身的范围,并可能涉及更复杂的资源管理和生命周期管理策略。
- 灵活性和可配置性(在某些实现中):某些单例模式的实现可能允许通过配置文件或环境变量来指定是否启用单例模式,或者允许在运行时通过某种方式改变单例实例的行为。然而,这种灵活性和可配置性并不是单例模式的固有特点,而是依赖于具体的实现方式。
- 简化复杂对象的访问:当某些对象的创建过程复杂或需要消耗大量资源时,使用单例模式可以简化对这些对象的访问。通过提供一个全局的访问点来访问这些复杂的对象,可以减少不必要的资源消耗和代码复杂性。
- 限制实例数量:单例模式最直接的效果就是限制了某个类的实例数量。这在某些场景下是非常有用的,比如数据库连接池、配置文件管理器等,这些类通常只需要一个实例就足够了。
二、单例模式的实现方式
在C++中,单例模式是一种确保一个类只有一个实例,并提供一个全局访问点来访问该实例的设计模式。单例模式有多种实现方式,每种方式都有其特定的应用场景和优缺点。以下是几种常见的C++单例模式实现方式。
2.1. 懒汉式(Lazy Initialization)
- 实现方式:懒汉式单例模式在第一次调用
getInstance()
方法时创建实例。 - 优点:
- 实现了延迟加载,节省资源。
- 在第一次调用时才实例化,有助于节约内存。
- 缺点:
- 线程不安全,在多线程环境下可能会创建多个实例。
- 懒汉式实现需要加锁,可能会影响性能。
- 示例代码(C++11及以后版本,使用
std::call_once
):
#include <iostream>
#include <mutex>
#include <memory>
class Singleton {
private:
Singleton() { std::cout << "Singleton constructed." << std::endl; }
static std::once_flag initInstanceFlag;
static std::unique_ptr<Singleton> instance;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
public:
static Singleton& getInstance() {
std::call_once(initInstanceFlag, []() {
instance.reset(new Singleton());
});
return *instance;
}
void someMethod() { std::cout << "Method of the singleton" << std::endl; }
~Singleton() { std::cout << "Singleton destructed." << std::endl; }
};
std::once_flag Singleton::initInstanceFlag;
std::unique_ptr<Singleton> Singleton::instance;
2.2. 饿汉式(Eager Initialization)
- 实现方式:饿汉式单例模式在类加载时就完成了实例的创建。
- 优点:
- 线程安全,因为实例在类加载时就已创建,无需考虑多线程同步问题。
- 响应速度快,因为实例已经存在,无需再进行创建。
- 缺点:
- 资源效率不高,如果实例在程序运行期间从未被使用,则会浪费资源。
- 不支持延迟加载。
- 示例代码:
class Singleton {
private:
static Singleton instance;
Singleton() {}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
public:
static Singleton& getInstance() {
return instance;
}
void someMethod() { /* ... */ }
};
Singleton Singleton::instance;
2.3. 双重检查锁定(Double-Checked Locking)
- 实现方式:双重检查锁定是一种在懒汉式基础上加入双重判断的优化方式,以减少锁的粒度,提高性能。
- 优点:
- 线程安全。
- 延迟加载,节约资源。
- 性能优于单纯的懒汉式(通过减少锁的粒度)。
- 缺点:
- 实现复杂,容易出错。
- 需要对volatile关键字和内存模型有深入理解。
- 示例代码(C++11及以后版本):
#include <mutex>
class Singleton {
private:
static Singleton* instance;
static std::mutex mtx;
Singleton() {}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
public:
static Singleton* getInstance() {
if (instance == nullptr) {
std::lock_guard<std::mutex> guard(mtx);
if (instance == nullptr) {
instance = new Singleton();
}
}
return instance;
}
// 其他成员和方法...
};
Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mtx;
2.4. 静态内部类(Static Inner Class)
- 实现方式:利用静态内部类实现单例模式,可以保证单例的线程安全和懒加载。
- 优点:
- 线程安全。
- 懒加载。
- 实现简单。
- 缺点:
- 依赖于JVM的类加载机制,可能在某些特定情况下不适用。
- 示例代码(C++):
class LazySingleton {
private:
LazySingleton() {}
class InnerHolder {
public:
static LazySingleton INSTANCE;
};
static LazySingleton& getInstance() {
return InnerHolder::INSTANCE;
}
// 禁止拷贝和赋值
LazySingleton(const LazySingleton&) = delete;
LazySingleton& operator=(const LazySingleton&) = delete;
};
LazySingleton LazySingleton::InnerHolder::INSTANCE;
2.5. 基于枚举的单例模式
- 实现方式:利用枚举的特殊性(枚举的实例在类加载时就被创建,且不能被反射破坏)来实现单例模式。
- 优点:
- 线程安全。
- 防止反射破坏。
- 实现简单。
- 缺点:不支持懒加载。
- 注意:C++标准库中并没有直接支持基于枚举的单例模式,这种方式更多是在Java等语言中应用。
三、线程安全
在多线程环境下,单例模式的实现需要确保线程安全。除了使用互斥锁等同步机制外,从C++11开始,局部静态变量的初始化被标准规定为线程安全。因此,可以使用静态局部变量来实现线程安全的单例模式,而无需显式使用互斥锁(如std::mutex
)或其他同步机制。
利用C++11的线程安全局部静态变量初始化来实现单例模式是一种简洁且高效的方法。这种方法通常被称为“Meyers' Singleton”(以Scott Meyers命名,他在其著作中推广了这种方法)。
下面是一个使用C++11的线程安全局部静态变量初始化来实现单例模式的示例:
#include <iostream>
class Singleton {
private:
Singleton() {
// 构造函数,可能包含一些初始化代码
std::cout << "Singleton constructed." << std::endl;
}
// 禁止拷贝和赋值
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
public:
// 静态成员函数,返回单例的引用
static Singleton& getInstance() {
// 局部静态变量,其初始化是线程安全的
static Singleton instance;
return instance;
}
void someMethod() {
// 单例的方法
std::cout << "Method of the singleton" << std::endl;
}
~Singleton() {
// 析构函数,可能包含一些清理代码
std::cout << "Singleton destructed." << std::endl;
}
};
int main() {
// 从多个线程访问单例
// 注意:为了简单起见,这里没有在main函数中直接创建线程
// 但在实际应用中,会从多个线程调用Singleton::getInstance()
// 访问单例
Singleton& singleton = Singleton::getInstance();
singleton.someMethod();
// 如果从多个线程调用,每个线程都会得到相同的实例
return 0;
}
在这个例子中,getInstance()
函数内的static Singleton instance;
声明了一个局部静态变量。根据C++11及以后的标准,这个局部静态变量的初始化是线程安全的。也就是说,即使多个线程同时调用getInstance()
,它们也会看到相同的instance
对象,且instance
的构造函数只会被调用一次。
这种方法的好处是代码简洁且易于理解,同时避免了使用互斥锁可能带来的性能开销。然而,需要注意的是,如果单例的构造函数或析构函数中执行了复杂的操作(如动态内存分配、文件操作等),则这些操作本身可能需要额外的同步措施来确保线程安全。但在这个上下文中,我们主要关注的是单例实例的创建和访问过程的线程安全性。