设计模式11:单例模式(全局唯一)

系列总链接:《大话设计模式》学习记录_net 大话设计-CSDN博客

参考:

C++ 设计模式------设计模式总结_c++设计模式 王建伟pdf-CSDN博客
C++ 设计模式------单例模式_c++单例模式-CSDN博客
C++特殊类设计1 单例模式_c++单例模式 饱汉-CSDN博客

一:概述

单例模式(Singleton Pattern)是软件工程中的一种设计模式,属于创建型模式。它提供了一种创建对象的最佳数量的方法------即确保一个类只有一个实例,并提供一个全局访问点来访问这个实例。使用单例模式的主要目的是控制资源共享,例如数据库连接、线程池等。

二:结构与实现

结构:

单例模式的结构可以简化为三个关键部分:

私有构造函数: 防止外部通过new创建实例。
静态实例变量: 保存类的唯一实例。
公共静态方法(如getInstance()): 提供全局访问点来获取该唯一实例。
简化的示意图如下:

cpp 复制代码
+---------------------+
| Singleton           |
+---------------------+
| - instance: Singleton|
+---------------------+
| + getInstance(): Singleton|
+---------------------+
  • instance 是私有的静态变量,存储单例对象。
  • getInstance() 是公共静态方法,用于返回单例对象。首次调用时创建对象,之后始终返回同一对象。

这种方式确保了类在系统中只有一个实例,并提供了全局访问点。

实现:

实现可以分为:饿汉和饱汉(又叫"懒汉")两种思路;

饿汉模式 (Eager Initialization)

  • 在饿汉模式下,单例类的实例在类加载时就被创建。这意味着无论该单例是否会被使用,都会占用内存资源。
  • 优点:由于对象的创建是在类加载的时候就完成了,所以当需要使用这个单例对象时,可以立即获取到,没有同步开销。
  • 缺点:如果程序运行期间从未使用过这个单例,那么它的初始化将是一种浪费,因为它占用了不必要的内存空间。
cpp 复制代码
class GameConfig {
private:
    GameConfig() {};
    static GameConfig* m_instance;

public:
    static GameConfig* getInstance() {
        return m_instance;
    }
};

GameConfig* GameConfig::m_instance = new GameConfig();

或者

cpp 复制代码
class Singleton
{
public:
    static Singleton* GetInstance()
    {
        return &m_instance;
    }
private:
    // 构造函数私有
    Singleton()
    {
        cout<<"Create Singleton Obj."<<endl;
    }
    // 构造函数私有
    ~Singleton()
    {
        cout<<"delete Singleton Obj."<<endl;
    }

    // C++98 防拷贝
    Singleton(Singleton const&);
    Singleton& operator=(Singleton const&);
    static Singleton m_instance;
};
Singleton Singleton::m_instance;

饱汉模式 (Lazy Initialization)

  • 饱汉模式也称为懒加载或懒汉模式,在这种模式下单例类的实例是在第一次使用时才被创建。
  • 优点:这种方式可以确保只有在真正需要使用单例的时候才会创建它,节省了系统资源。
  • 缺点:如果在高并发环境下,可能会出现多个线程同时尝试创建单例的情况,这就需要额外的同步机制来保证线程安全。

c++对于线程安全和延迟加载优化后的单例模式,可以采用的方式:

  • 双重检查锁定:这种方法结合了性能和线程安全性。它首先检查实例是否存在,如果不存在,则使用锁机制确保只有一个线程能够创建实例。
cpp 复制代码
#include <QObject>
#include <QDebug>
#include <QMutex>

/*
    饱汉模式:
    1.基础版:会存在线程同步问题;
            ||
    2.加mutex,单nullptr判断:会增加不必要的锁访问阻塞时间
            ||
    3.加mutex,两nullptr判断(双重检查锁定):避免不必要的锁访问阻塞
*/
class SingleTon
{
public:
    explicit SingleTon(){
        qDebug() << "SingleTon create.";
    }

    static SingleTon* m_pInstance;
    static QMutex mutex;

    static SingleTon* getInstance(){
        if(m_pInstance == nullptr){
            QMutexLocker locker(&mutex);
            if(m_pInstance == nullptr){
                m_pInstance = new SingleTon();
            }
        }
        return m_pInstance;
    }

    static void freeInstance(){
        if(m_pInstance != nullptr){
            delete m_pInstance;
            m_pInstance = nullptr;
        }
    }

private:
    ~SingleTon(){
        qDebug() << "SingleTon destruct.";
    }
    SingleTon(const SingleTon&) = delete;
    SingleTon& operator =(const SingleTon&) = delete;

};
SingleTon* SingleTon::m_pInstance = nullptr;
QMutex SingleTon::mutex;
  • 静态局部变量(Static Local Variable): C++11标准保证了静态局部变量的线程安全初始化。当函数第一次被调用时,静态局部变量会被初始化,并且这种初始化是线程安全的。
cpp 复制代码
/*
    饱汉模式:
    4.局部静态变量:内存自动释放,不用人为管理
*/
class GameConfig
{
public:
    static GameConfig& getInstance() {
        static GameConfig instance; // 自动管理生命周期
        return instance;
    }

private:
    GameConfig() {
        qDebug() << "GameConfig contruct.";
    }
    ~GameConfig() {
        qDebug() << "GameConfig destruct.";
    }
    GameConfig(const GameConfig&) = delete;
    GameConfig& operator=(const GameConfig&) = delete;
};

考虑到内存释放问题:上述第一种为手动释放(调用freeInstance),第二种为自动释放,也可以增加内部嵌套类,如:

cpp 复制代码
/*
    饱汉模式:
    5.嵌套类与内存管理: 定义垃圾回收类,通过该类对象自动释放单例类对象
*/

namespace singleHungryMan
{

class GameConfig {
private:
    GameConfig() {
        qDebug() << "singleFullMan::GameConfig contruct.";
    }
    GameConfig(const GameConfig&) = delete;
    GameConfig& operator=(const GameConfig&) = delete;
    ~GameConfig() {
        qDebug() << "singleFullMan::GameConfig destruct.";
    } // 私有析构函数

public:
    static GameConfig* getInstance() {
        if(m_instance == nullptr){
            QMutexLocker locker(&mutex);
            if(m_instance == nullptr){
                m_instance = new GameConfig();
            }
        }
        return m_instance;
    }

private:
    static GameConfig* m_instance; // 指向单例对象的指针
    static QMutex mutex;

    // 垃圾回收类
    class Garbo {
    public:
        Garbo(){
            qDebug() << "singleFullMan::GameConfig::Garbo contruct.";
        }
        ~Garbo() {
            qDebug() << "singleFullMan::GameConfig::Garbo destruct.";

            if (GameConfig::m_instance != nullptr) {
                delete GameConfig::m_instance; // 释放内存
                GameConfig::m_instance = nullptr; // 避免悬空指针
            }
        }
    };

    static Garbo garboobj; // 静态Garbo对象
};

// 静态成员变量初始化
GameConfig* GameConfig::m_instance = nullptr; // 在类外初始化
QMutex GameConfig::mutex;
GameConfig::Garbo GameConfig::garboobj; // 创建Garbo对象
}

工程如果多个类都需要单例对象,可以考虑定义模板单例类,如:

cpp 复制代码
/*
    延伸:
    6.定义一个单例的模板类,随后用该模板类和我们要创建的类实例化一个特定的类对象。这个模板类其实
    我和我们上面实现的单例类是一样的,只是将其变为模板(这个模板类目前还不是线程安全,要想达到
    线程安全,就必须和我们刚才讲的一样,加锁)。
*/
#include <iostream>
#include <mutex>
using std::cout;
using std::endl;

namespace cc
{
    template<typename T>
    class Singleton
    {
        public:
            static T * getInstance();
            static void destroyInstance();
        private:
            Singleton(){cout << "cc::Singleton()" << endl;}
            ~Singleton(){cout << "cc::~Singleton()" << endl;}
            Singleton(const Singleton &) = delete;
            Singleton & operator=(const Singleton &) = delete;
        private:
            static std::mutex m_mutex;
            static T * m_pInstance;
    };

    template<typename T>
    T* Singleton<T>::m_pInstance = nullptr;

    template<typename T>
    std::mutex Singleton<T>::m_mutex;

    template<typename T>
    T* Singleton<T>::getInstance(){
        if(nullptr == m_pInstance){
            std::lock_guard<std::mutex> guard(m_mutex);
            if(nullptr == m_pInstance){
                cout << "cc::getInstance()" << endl;
                m_pInstance = new T;
            }
        }
        return m_pInstance;
    }

    template<typename T>
    void Singleton<T>::destroyInstance(){
        if(m_pInstance != nullptr){
            cout << "cc::destroyInstance()" << endl;
            delete m_pInstance;
            m_pInstance = nullptr;
        }
    }
}//end of namespace

这两种模式各有优劣,选择哪种方式取决于具体的使用场景和需求。如果你确定单例会在应用程序启动后不久被使用,那么饿汉模式可能是一个更好的选择;而如果你希望推迟初始化直到确实需要为止,并且你的应用处于多线程环境中,那么饱汉模式可能是更合适的选择。

三:应用

1.数据库对象创建,使用;

2.线程池管理;

3.日志管理;

4.配置管理;

5.各种业务管理类对象;

四:优缺点及适用环境

优点:

  1. **唯一实例:**确保一个类只有一个实例,这对于需要统一管理资源的情况非常有用,比如数据库连接池、线程池等。
  2. **全局访问点:**提供了一个全局访问点,使得对象可以在任何地方被方便地访问,简化了配置管理和资源共享。
  3. **延迟初始化:**可以实现按需加载(懒加载),在第一次使用时才创建实例,节省系统资源。

缺点:

  1. **隐藏依赖关系:**单例模式可能隐藏了类之间的依赖关系,这会使得代码难以理解和维护,并且不利于单元测试。
  2. **破坏单一职责原则:**单例类不仅负责自身的业务逻辑,还承担了管理自身生命周期的责任,违反了单一职责原则。
  3. **线程安全问题:**如果处理不当,在多线程环境下可能会引发竞态条件,导致创建多个实例的问题。
  4. **难以扩展:**因为构造函数是私有的,所以不能通过继承来扩展单例类,增加了设计上的限制。

适用环境:

  • **需要全局访问的对象:**如配置文件读取器、日志记录器等,这些服务通常在整个应用程序中只需要一个共享实例。
  • **控制资源访问:**当需要严格控制对某些资源(如数据库连接、文件句柄)的访问时,单例模式可以帮助保证同一时间只有一个实例与资源交互。
  • **减少内存占用和提高性能:**对于那些创建成本高或频繁使用的对象,单例模式可以通过重用唯一的实例来减少内存消耗并提升效率。
  • **避免重复初始化:**防止同一个对象多次初始化,特别是在初始化过程复杂或者耗时的情况下。

总的来说,单例模式适合于那些确实需要保证全局唯一性并且需要高效利用资源的情景。然而,由于其固有的局限性和潜在的设计问题,在选择使用单例模式之前应该仔细考虑是否真的必要以及是否有更好的替代方案。

相关推荐
渣哥1 小时前
原来 Java 里线程安全集合有这么多种
java
间彧1 小时前
Spring Boot集成Spring Security完整指南
java
间彧1 小时前
Spring Secutiy基本原理及工作流程
java
数据智能老司机2 小时前
精通 Python 设计模式——创建型设计模式
python·设计模式·架构
Java水解2 小时前
JAVA经典面试题附答案(持续更新版)
java·后端·面试
数据智能老司机3 小时前
精通 Python 设计模式——SOLID 原则
python·设计模式·架构
洛小豆5 小时前
在Java中,Integer.parseInt和Integer.valueOf有什么区别
java·后端·面试
前端小张同学5 小时前
服务器上如何搭建jenkins 服务CI/CD😎😎
java·后端
ytadpole5 小时前
Spring Cloud Gateway:一次不规范 URL 引发的路由转发404问题排查
java·后端
华仔啊5 小时前
基于 RuoYi-Vue 轻松实现单用户登录功能,亲测有效
java·vue.js·后端