实战设计模式之单例模式

概述

在进行大型项目的系统架构设计时,确保某些类只有一个实例,是非常重要的。比如:日志记录器、数据库连接池、配置管理器等组件,通常只需要一个实例来处理所有请求。在这种情况下,如果每次使用都创建新的对象实例,不仅会浪费系统资源,还可能导致数据不一致。为了解决这一问题,单例模式应运而生。

基本原理

单例模式的核心在于控制类的实例化过程,确保在整个应用程序生命周期内只存在一个实例,并提供一个全局访问点。为了达到这个目的,单例模式通常包括以下几个核心特征。

私有化构造函数:防止外部代码直接调用构造函数创建新实例。

静态成员变量:用于存储唯一的类实例。

公共静态方法:提供获取唯一实例的方法,通常是Singleton函数或GetInstance函数。

懒加载机制:只有当需要时才创建实例,以提高性能。

线程安全

需要特别注意的是,单例模式在多线程环境下可能会出现问题。具体来说,如果两个线程几乎同时调用了 Singleton函数或GetInstance函数,那么就可能出现两个线程都判断静态成员变量为NULL,并分别创建新实例的情况,从而违反了单例模式的基本要求。

为了避免这种情况,我们可以引入线程同步机制。一种常见做法是在Singleton函数或GetInstance函数中使用互斥锁,以确保同一时间只有一个线程能够进入临界区。然而,这样做会带来额外的性能开销,尤其是在高并发场景下。因此,更推荐的做法是采用双重检查锁定技术,它可以在保持线程安全的同时尽量减少锁的竞争。具体如何使用,可以参考下面的示例代码。

cpp 复制代码
#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函数。

cpp 复制代码
#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函数。

cpp 复制代码
#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;
}
相关推荐
m0_663234011 小时前
计算机网络ENSP课设--三层架构企业网络
网络·计算机网络·架构
Vincent(朱志强)1 小时前
设计模式详解(十):策略模式——Strategy
设计模式·策略模式
WangMing_X2 小时前
C# 23种设计模式(1)单例模式(单件模式)
单例模式·设计模式
程序猿进阶2 小时前
可视化逻辑表达式编辑器
java·spring boot·后端·面试·性能优化·架构·编辑器
友艺2 小时前
【GrpahRAG】图谱增量更新技术对比及源码分析
人工智能·架构
wkj0012 小时前
PHP开发设计模式:单例模式
单例模式·设计模式·php
MavenTalk3 小时前
说说聊聊CNCF(云原生计算基金会)
微服务·云原生·架构·kubernetes·cloud native·cncf
乐闻x3 小时前
如何使用 Webpack ModuleFederationPlugin 构建微前端架构
前端·webpack·架构
不爱吃米饭_3 小时前
工作中常用的8种设计模式
设计模式
岳轩子4 小时前
23种设计模式之命令模式
设计模式·命令模式