单例模式 (Singleton Pattern) 是一种创建型设计模式,确保一个类只有一个实例,并提供一个全局访问点。
一、基础
1. 意图
- 确保一个类只有一个实例。
- 提供一个全局访问点。
2. 适用场景
- 一个类只需要一个实例来协调系统行为时,例如数据库连接池,线程池、缓存、日志对象等。
- 需要控制实例数目,节省系统资源,避免重复创建和浪费, 同时保证数据的一致性和正确性。
3. 结构
- 一个静态成员变量
- 一个私有构造函数
- 一个静态方法
二、进阶
1. 线程安全
- 饿汉式: 在类加载时就创建实例,线程安全,但可能会造成资源浪费。
- 懒汉式: 在第一次调用 getInstance() 时才创建实例,需要考虑线程安全问题。
- 双重检查锁定: 使用 volatile 关键字和 synchronized 关键字来保证线程安全。
- 静态内部类: 利用类加载机制保证线程安全。
2. 序列化与反序列化
在 C++ 中,并没有像 Java 那样内置的序列化机制。但是,如果使用第三方库(如 Boost.Serialization)进行序列化和反序列化操作,同样需要注意单例模式的问题。在反序列化时,可能会创建新的实例对象,破坏单例模式。为了避免这种情况,可以在反序列化过程中进行特殊处理,确保返回的是已经存在的单例实例。
3. 反射攻击
为了防止反射调用私有构造函数创建新的实例,可以在构造函数中进行判断。
三、关键点
1. 私有构造函数:单例类的构造函数必须是私有的,以防止外部通过new关键字实例化对象,确保只有一个实例对象存在。
2. 静态实例变量:单例类中必须有一个静态的实例变量来保存唯一的实例对象,并且该实例变量的作用域必须是私有的,以防止外部直接访问和修改。
3. 公共静态访问方法:提供一个公共的静态方法,作为全局访问点,用于获取单例对象的实例。该方法通常命名为getInstance()。
4. 线程安全:在多线程环境下,单例模式的实现必须保证线程安全,确保在任何情况下都只会创建一个实例对象。常见的线程安全实现方式有同步方法、双重检查锁定、静态局部变量等。
5. 延迟加载:对于某些应用场景,延迟加载(懒汉式)是非常重要的,它可以避免在程序启动时就创建不必要的实例对象,从而提高系统性能和资源利用率。但需要注意的是,懒汉式实现需要处理好线程安全问题。
四、易错点
1. 线程安全问题:在实现懒汉式单例模式时,如果不注意线程安全,很容易导致在多线程环. 境下创建多个实例对象,违反单例模式的定义。例如,在没有同步机制的情况下,多个线程同时. 调用getInstance()方法,并且此时instance为nullptr,就会创建多个实例对象。
2. 序列化与反序列化:当使用第三方库进行序列化和反序列化操作时,如果不处理好单例模式的问题,可能会导致在反序列化时创建新的实例对象,破坏单例模式。需要在反序列化过程中进行特殊处理,确保返回的是已经存在的单例实例。
3. 反射破坏:虽然 C++ 没有像 Java 那样强大的反射机制,但是通过一些技术手段也可以实现类似反射的功能。如果不小心使用这些技术调用了单例类的私有构造函数,同样会破坏单例模式。为了防止这种情况,需要在构造函数中添加逻辑判断,防止重复创建实例对象。
4. 指令重排:在使用双重检查锁定实现线程安全的懒汉式单例模式时,需要注意编译器和处理器的指令重排问题。虽然 C++11 标准中引入了内存模型来解决这个问题,但是在一些老版本的编译器或者特定的硬件平台上,仍然可能存在指令重排导致的线程安全问题。
五、核心代码
1. 饿汉式
饿汉式是单例模式中最简单的一种实现方式。在程序启动时,就立即创建唯一的实例对象,并且该实例对象是静态的,因此在程序的整个生命周期中只会存在一个实例。
优点:实现简单,线程安全,因为在程序启动时就创建了实例,不存在多线程并发创建的问题。
缺点:如果该单例对象在整个应用程序中不一定会被使用到,那么在程序启动时就创建实例可能会造成资源浪费。
class Singleton {
private:
static Singleton* instance;
Singleton() {} // 私有构造函数
public:
static Singleton* getInstance() {
return instance;
}
};
Singleton* Singleton::instance = new Singleton(); // 类加载时创建实例
2. 懒汉式 (双重检查锁定)
双重检查锁定(Double-Checked Locking)是一种优化的线程安全懒汉式实现方式。它通过两次检查instance是否为nullptr,在保证线程安全的同时提高了性能。
优点:延迟加载,只有在真正需要使用单例对象时才创建,避免了不必要的资源浪费。既实现了线程安全,又提高了性能。在第一次检查instance为nullptr时,只有一个线程能够进入同步块,在同步块内再次检查instance为nullptr时才创建实例对象,避免了每次调用getInstance()方法都获取锁的性能开销。
缺点:实现相对复杂,需要理解双重检查锁定的原理以及互斥锁的使用。
class Singleton {
private:
static Singleton* instance;
static std::mutex mtx;
Singleton() {} // 私有构造函数
public:
static Singleton* getInstance() {
if (instance == nullptr) {
std::lock_guard<std::mutex> lock(mtx);
if (instance == nullptr) {
instance = new Singleton();
}
}
return instance;
}
};
Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mtx;
3. 懒汉式 (静态内部类)
在 C++11 之后,可以利用静态局部变量的特性来实现单例模式,这种方式既实现了延迟加载,又保证了线程安全。
优点:延迟加载,线程安全。由于静态局部变量在第一次调用getInstance()方法时才会被初始化,因此实现了延迟加载。同时,C++11 标准保证了静态局部变量的初始化是线程安全的。
缺点:无明显缺点。
class Singleton {
private:
Singleton() {} // 私有构造函数
static class SingletonHolder {
public:
static Singleton* instance;
};
public:
static Singleton* getInstance() {
return SingletonHolder::instance;
}
};
4. 防止序列化与反序列化
class Singleton {
private:
static Singleton* instance;
Singleton() {} // 私有构造函数
public:
static Singleton* getInstance() {
if (instance == nullptr) {
instance = new Singleton();
}
return instance;
}
// 防止反序列化创建新的实例
Singleton* readResolve() {
return getInstance();
}
};
Singleton* Singleton::instance = nullptr;
5. 防止反射攻击
class Singleton {
private:
static Singleton* instance;
Singleton() {
if (instance != nullptr) {
throw std::runtime_error("Singleton instance already exists!");
}
} // 私有构造函数
public:
static Singleton* getInstance() {
if (instance == nullptr) {
instance = new Singleton();
}
return instance;
}
};
Singleton* Singleton::instance = nullptr;
六、总结
单例模式是一种常用的设计模式,但需要注意线程安全、序列化与反序列化、反射攻击等问题。在实际开发中,需要根据具体需求选择合适的实现方式。