实战设计模式之单例模式
在进行大型项目的系统架构设计时,确保某些类只有一个实例,是非常重要的。比如:日志记录器、数据库连接池、配置管理器等组件,通常只需要一个实例来处理所有请求。在这种情况下,如果每次使用都创建新的对象实例,不仅会浪费系统资源,还可能导致数据不一致。为了解决这一问题,单例模式应运而生。
概述
在进行大型项目的系统架构设计时,确保某些类只有一个实例,是非常重要的。比如:日志记录器、数据库连接池、配置管理器等组件,通常只需要一个实例来处理所有请求。在这种情况下,如果每次使用都创建新的对象实例,不仅会浪费系统资源,还可能导致数据不一致。为了解决这一问题,单例模式应运而生。
基本原理
单例模式的核心在于控制类的实例化过程,确保在整个应用程序生命周期内只存在一个实例,并提供一个全局访问点。为了达到这个目的,单例模式通常包括以下几个核心特征。
私有化构造函数:防止外部代码直接调用构造函数创建新实例。
静态成员变量:用于存储唯一的类实例。
公共静态方法:提供获取唯一实例的方法,通常是Singleton函数或GetInstance函数。
懒加载机制:只有当需要时才创建实例,以提高性能。
线程安全
需要特别注意的是,单例模式在多线程环境下可能会出现问题。具体来说,如果两个线程几乎同时调用了 Singleton函数或GetInstance函数,那么就可能出现两个线程都判断静态成员变量为NULL,并分别创建新实例的情况,从而违反了单例模式的基本要求。
为了避免这种情况,我们可以引入线程同步机制。一种常见做法是在Singleton函数或GetInstance函数中使用互斥锁,以确保同一时间只有一个线程能够进入临界区。然而,这样做会带来额外的性能开销,尤其是在高并发场景下。因此,更推荐的做法是采用双重检查锁定技术,它可以在保持线程安全的同时尽量减少锁的竞争。具体如何使用,可以参考下面的示例代码。
#include <iostream>
#include <mutex>
using namespace std;
class CLogger
{
public:
static CLogger *Singleton()
{
if (s_pSingleton == NULL)
{
// 第一次检查
lock_guard<mutex> lock(s_mutexLog);
if (s_pSingleton == nullptr)
{
// 第二次检查
s_pSingleton = new CLogger();
}
}
return s_pSingleton;
}
void Info(const string& strMsg)
{
cout << "Info: " << strMsg << endl;
}
private:
CLogger() {}
static CLogger* s_pSingleton;
static mutex s_mutexLog;
};
CLogger* CLogger::s_pSingleton = NULL;
mutex CLogger::s_mutexLog;
int main()
{
CLogger::Singleton()->Info("Hello, Hope_Wisdom");
return 0;
}
在上面的示例代码中,我们实现了一个可多线程使用的日志单例类。通过两次检查s_pSingleton是否为NULL,我们确保了即使多个线程同时到达临界区,也只会有一个线程真正执行创建实例的操作。
实战解析
假如我们有很多个类都需要实现单例功能,那么,每个类都像上面的日志类CLogger那样去实现,将是非常繁琐而冗余的。能否编写一个单例基类,其他需要实现单例功能的类,都从该基类派生呢?
答案是肯定的。实现一个通用的单例模式基类,可以让其他类继承它以获得单例行为,这是一种优雅且可复用的设计方法。在C++中,我们可以通过模板来创建一个泛型的单例基类,这样可以确保任何派生类都能自动具备单例特性。
除了创建实例外,单例模式还需要考虑如何正确地销毁实例,以避免内存泄漏。在C++中,由于没有垃圾回收机制,我们必须手动去管理对象的生命周期。对于单例模式,通常有以下两种方式来处理这个问题。
1、全局静态对象:将单例实例声明为全局静态变量,这样它会在程序启动时自动初始化,并在程序结束时自动销毁。这种方式虽然简单易行,但缺乏灵活性,特别是当单例依赖其他资源或其他单例时(无法保证销毁的先后顺序)。
2、动态创建 + 显式销毁:使用new关键字动态分配内存给单例实例,并在适当的时候通过delete来释放。
综合以上这些考虑,我们在下面的BaseSingleton.h文件中实现了一个单例模式的基类模板。它提供了三个静态成员函数:Open函数用于动态创建单例对象,Close函数用于显式销毁单例对象,Singleton函数用于获取单例对象的指针。
在下面的实现中,没有使用锁来保护单例对象,这是我们特意为之的。原因有二:一是可以省去锁的开销;二是创建和销毁的接口相对应,提醒用户在主程序的最开始和结束处调用Open和Close函数。
#pragma once
template<typename Derived>
class CBaseSingleton
{
public:
static void Open()
{
if (s_pSingleton == NULL)
{
s_pSingleton = new Derived();
}
}
static void Close()
{
if (s_pSingleton != NULL)
{
delete s_pSingleton;
s_pSingleton = NULL;
}
}
static Derived *Singleton()
{
return s_pSingleton;
}
virtual ~CBaseSingleton() {}
protected:
CBaseSingleton() {}
CBaseSingleton(const CBaseSingleton&) = delete;
CBaseSingleton& operator=(const CBaseSingleton&) = delete;
private:
static Derived* s_pSingleton;
};
template<typename Derived>
Derived* CBaseSingleton<Derived>::s_pSingleton = NULL;
接下来,我们重新实现了CLogger类。可以看到,CLogger类单例功能的实现非常简单,从CBaseSingleton<CLogger>类派生,并声明下友元类即可。在main函数中,我们在最开始调用了Open函数,然后使用Singleton函数访问单例对象,最后在结束处调用了Close函数。
#include <iostream>
#include <string>
using namespace std;
#include "BaseSingleton.h"
class CLogger : public CBaseSingleton<CLogger>
{
friend class CBaseSingleton<CLogger>;
public:
void Info(const string& strMsg)
{
cout << "Info: " << strMsg << endl;
}
private:
CLogger() {}
};
int main()
{
CLogger::Open();
CLogger::Singleton()->Info("Hello, Hope_Wisdom");
CLogger::Close();
return 0;
}
更多推荐
所有评论(0)