介绍
单例模式是一种设计模式,用于确保一个类只能有一个实例,并提供一个全局访问点来访问该实例。在单例模式中,类的构造函数是私有的(private),这意味着该类不能直接实例化。相反,类提供了一个静态方法或静态变量,用于获取该类的唯一实例。
单例模式使用场景
- 控制对唯一实例的访问:某些情况下,确保系统中只有一个实例是必要的,比如日志记录器、数据库连接、线程池等。使用单例模式可以确保这些资源只被创建一次,并提供一个统一的访问点。
- 全局状态共享:单例模式可以用于在应用程序中共享全局状态或配置信息,无需传递实例或参数。
- 延迟实例化:在需要的时候才创建,而不是在应用程序启动的时候就创建实例对象。这样可以节省资源并且提高性能。
几种实现方式
如何确保一个类同时只能拥有一个实例对象,一种办法是如果检测到有新的创建同一个类的需求时,检测该类已经有对象存在的情况下直接返回退出,即不创建。另一种方式是,在类加载时就被初始化为一个新的实例,此后只存在一个全局静态的访问点去使用这个实例。且该实例对象必须是私有的,保证只有通过这个类的成员函数才能够访问或创建对象。
懒汉式
顾名思义,能不创建对象的时候,就不去分配资源。只有在实际使用的时候,采取创建和分配对象。
这里通过getInstance()成员方法检查静态成员变量instance是否为空,若为空,则new一个新的实例并返回,否则直接返回现有的实例。
// 懒汉式单例模式
class LazySingleton {
private:
static LazySingleton* instance;
LazySingleton() {} // 私有构造函数
public:
static LazySingleton* getInstance() {
if (instance == nullptr) {
instance = new LazySingleton();
}
return instance;
}
};
LazySingleton* LazySingleton::instance = nullptr;
饿汉式
饿汉即没吃饱看到就吃,只要类加载时就被初始化。静态成员变量 instance 在类加载时就被初始化为一个新的实例。
// 饿汉式单例模式
class EagerSingleton {
private:
static EagerSingleton* instance;
EagerSingleton() {} // 私有构造函数
public:
static EagerSingleton* getInstance() {
return instance;
}
};
EagerSingleton* EagerSingleton::instance = new EagerSingleton();
这里可以看到他们的区别在于通过getInstance()获取实例对象时创建,还是在初始化instance变量的同时立即创建。
问题
在实际多线程的环境下,饿汉式的实现方式由于静态成员变量instance在类加载的时候就被初始化。由于类加载时是线程安全的,因此不需要额外的同步措施。这种实现方式避免了懒汉式中的线程安全问题,但可能会增加程序的启动时间和资源消耗。
如果此时,多个线程同时使用上述懒汉式的方式调用getInstance()方法时,如果instance()尚未创建,他们可能同时通过了instance == nullptr的检查,然后各自创建了一个新的实例,导致最终存在多个实例。
为了解决多线程环境下的安全性问题,可以采用锁的机制,确保同一时间只有一个线程可以创建实例。比如,互斥锁(Mutex)、双重检查锁、原子操作。下面是用互斥锁的方式对原有懒汉式的单例模式的改进,并打印出两个实例的地址信息。用于检查创建的对象是否是唯一的。
#include <iostream>
#include <mutex>
class LazySingleton {
private:
static LazySingleton* instance;
static std::mutex mutex;
LazySingleton() {} // 私有构造函数
public:
static LazySingleton* getInstance() {
std::lock_guard<std::mutex> lock(mutex);
if (instance == nullptr) {
instance = new LazySingleton();
}
return instance;
}
};
LazySingleton* LazySingleton::instance = nullptr;
std::mutex LazySingleton::mutex;
int main() {
LazySingleton* singleton1 = LazySingleton::getInstance();
LazySingleton* singleton2 = LazySingleton::getInstance();
std::cout << "singleton1 address: " << singleton1 << std::endl;
std::cout << "singleton2 address: " << singleton2 << std::endl;
return 0;
}
在这个示例中,通过使用 std::mutex 来确保在多线程环境下对 instance 的访问是安全的。使用 std::lock_guard 来管理互斥锁的生命周期,从而在退出作用域时自动释放锁。
打印结果:

双重检查锁机制
即在原有增加互斥锁的基础上,多增加一条判断instance是否为空的条件。然后使用mutex保护临界区,在临界区里再次检查instance是否为空,如果是,则创建一个新的实例。这种方式可以减少锁的持有时间,提高性能。
static LazySingleton* getInstance() {
if (instance == nullptr) {
std::lock_guard<std::mutex> lock(mutex);
if (instance == nullptr) {
instance = new LazySingleton();
}
}
return instance;
}
原子操作
使用 std::atomic 类型来声明 instance,并使用原子加载和存储操作来保证线程安全。使用 std::atomic_thread_fence 来确保内存顺序的正确性。
static LazySingleton* getInstance() {
LazySingleton* temp = instance.load(std::memory_order_acquire);
std::atomic_thread_fence(std::memory_order_acquire);
if (temp == nullptr) {
std::lock_guard<std::mutex> lock(mutex);
temp = instance.load(std::memory_order_relaxed);
if (temp == nullptr) {
temp = new LazySingleton();
std::atomic_thread_fence(std::memory_order_release);
instance.store(temp, std::memory_order_relaxed);
}
}
return temp;
}
最后
除了线程安全性之外,单例模式还有一些需要注意的地方和技术细节:
- 延迟初始化的影响:懒汉式单例模式延迟了实例的创建时间,这可能会在首次访问单例时引入额外的延迟。如果应用程序对启动时间或性能要求较高,可能需要权衡是否使用懒汉式单例模式。
- 内存泄漏风险:单例模式中的实例通常是静态的,并且在应用程序生命周期内一直存在。如果实例持有大量资源或长时间不被释放,可能会导致内存泄漏。因此,确保单例实例在不再需要时能够正确地释放资源是非常重要的。
- 多线程环境下的性能问题:在多线程环境下,单例模式的实现可能会引入额外的同步开销,从而影响性能。因此,在选择适当的同步机制时需要权衡性能和线程安全性。
- 序列化和反序列化:如果单例类需要支持序列化和反序列化操作,需要额外注意单例实例的保存和恢复。确保序列化操作不会破坏单例模式的约束,并且反序列化操作能够正确地恢复单例实例。
- 单例模式的替代方案:在某些情况下,使用依赖注入
Dependency Injection或者容器Container等技术可以替代单例模式。这样可以更灵活地管理实例的生命周期,并且避免了单例模式可能带来的一些问题。 - 测试:由于单例模式的实例是全局唯一的,因此在测试时可能会出现一些困难。可以通过依赖注入或者在单例类中提供额外的构造函数来解决这个问题,从而使单例实例可被替换或者模拟。
单例模式是一种常见的设计模式,但在使用时需要仔细考虑其线程安全性、延迟初始化、资源管理等方面的问题,并且需要根据具体情况选择合适的实现方式和替代方案。

本文介绍了单例模式的原理,探讨了其在控制唯一实例访问、全局状态共享和延迟实例化中的应用,并比较了懒汉式和饿汉式的实现方式。着重分析了多线程环境下的线程安全问题及其解决方案,包括使用互斥锁、双重检查锁和原子操作。同时,也讨论了单例模式的潜在问题和替代方案。

1199

被折叠的 条评论
为什么被折叠?



