首页 > 其他分享 >设计模式之单例模式

设计模式之单例模式

时间:2024-11-16 22:17:59浏览次数:3  
标签:std Singleton 模式 实例 线程 C++ 单例 设计模式

一、概念

单例模式(Singleton Pattern)是一种创建型设计模式,确保一个类只有一个实例,并提供全局访问点。在C++中,实现单例模式需要考虑到线程安全、延迟初始化以及避免全局对象初始化顺序问题等因素。

二、主要思想

单例模式的主要思想是控制类实例的数量并集中管理访问。它通过一个全局访问点来提供类的唯一实例,从而避免因为多次实例化导致的资源浪费或不一致性。这通常是通过私有化构造函数来实现的,以阻止外部通过new直接创建对象实例。相反,类本身负责创建自己的唯一实例,并确保所有外部对该实例的请求都返回同一个对象引用。

三、优缺点

优点

  1. 在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例时。
  2. 避免对资源的多重占用,可以方便地存储和控制状态信息。
  3. 提供了一个全局访问点,使得整个应用程序可以通过这一点访问单例对象,有助于更好地控制资源。

缺点

  1. 过度依赖单例模式可能使代码变得紧耦合和难以测试。
  2. 单例模式限制了类的实例化次数,这在一定程度上增加了扩展的难度。
  3. 如果单例类管理一些资源(如打开的文件、网络连接等),在析构函数中释放这些资源是一个合理的做法,但这也可能导致资源释放的顺序问题。

四、单例模式的分类及区别

4.1 单例模式的分类

在C++中,单例模式可以根据其实现方式和特性分为几种不同的类型,主要包括:

4.1.1 饿汉式

  • 在类加载时就完成了实例的初始化,因此是线程安全的。
  • 但无法实现延迟加载,可能造成资源浪费。
  • 应用场景:适用于在程序启动时就需要创建实例,且实例的创建开销不大,或者实例的创建不依赖于其他资源的情况。
  • 优点:实现简单,线程安全,因为实例在类加载时已经创建。
  • 缺点:无法实现延迟加载,如果实例在程序执行过程中并不需要,则会造成资源浪费。
#include <iostream>
#include <mutex>

class Singleton {
public:
    // 禁止拷贝构造函数和赋值操作符
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

    // 提供一个全局访问点来获取单例实例
    static Singleton& getInstance() {
        // 由于静态局部变量在第一次使用时初始化,并且只初始化一次,
        // 所以这里利用这个特性来创建单例实例。
        static Singleton instance;
        return instance;
    }

    // 其他成员函数
    void doSomething() {
        std::cout << "Doing something in the Singleton instance." << std::endl;
    }

private:
    // 私有构造函数,防止外部直接创建实例
    Singleton() {
        // 可以在这里进行资源初始化等操作
        std::cout << "Singleton instance created." << std::endl;
    }

    // 私有析构函数,防止外部删除实例(虽然在这个例子中由于静态局部变量的特性,析构函数不会被外部调用)
    ~Singleton() {
        std::cout << "Singleton instance destroyed." << std::endl;
    }
};

int main() {
    // 获取单例实例并调用其成员函数
    Singleton& singleton = Singleton::getInstance();
    singleton.doSomething();

    // 注意:由于实例是静态局部变量,在main函数结束时它会被自动销毁。
    // 但是,由于C++的静态存储期规则,这个销毁过程并不会显式地体现在代码中(即不会有显式的delete调用)。
    return 0;
}

在这个示例中,Singleton类有一个私有的构造函数和一个私有的析构函数,以防止外部直接创建或删除实例。getInstance方法提供了一个全局访问点来获取单例实例。由于getInstance方法中的静态局部变量instance在第一次调用时初始化,并且只初始化一次,因此它实现了单例模式。

需要注意的是,虽然这个示例在功能上类似于饿汉式单例,但严格来说,它并不是在类加载时立即创建实例(因为C++没有类加载的概念)。然而,由于静态局部变量的初始化特性,这个实现方式在效果上非常接近饿汉式单例,即实例在程序运行到getInstance方法时就已经存在,且只存在一个实例。

另外,由于C++11引入了线程安全的静态局部变量初始化,因此这个实现在多线程环境下也是安全的。

4.1.2 懒汉式

  • 在第一次调用时实例化,实现了延迟加载。
  • 但需要处理线程安全问题,如果处理不当,可能会导致多个实例被创建。
  • 应用场景:适用于在程序运行过程中按需创建实例,且实例的创建开销较大,或者实例的创建依赖于其他资源的情况。
  • 优点:可以实现延迟加载,节省资源。
  • 缺点:需要处理线程安全问题,如果多个线程同时调用getInstance方法,可能会导致多个实例被创建(非线程安全的懒汉式)。

以下是一个线程不安全的懒汉式单例模式的C++示例(仅用于教学目的,不推荐在实际使用): 

#include <iostream>

class Singleton {
public:
    // 禁止拷贝构造函数和赋值操作符
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

    // 提供一个全局访问点来获取单例实例
    static Singleton* getInstance() {
        if (instance_ == nullptr) { // 如果没有实例,则创建它
            instance_ = new Singleton();
        }
        return instance_;
    }

    // 其他成员函数
    void doSomething() {
        std::cout << "Doing something in the Singleton instance." << std::endl;
    }

    // 提供一个清理函数(在实际应用中,通常不需要手动调用,除非有特殊的资源管理需求)
    static void cleanup() {
        delete instance_;
        instance_ = nullptr;
    }

private:
    // 私有构造函数,防止外部直接创建实例
    Singleton() {
        std::cout << "Singleton instance created." << std::endl;
    }

    // 私有析构函数,防止外部删除实例(在这个例子中,我们提供了一个cleanup函数来管理资源)
    ~Singleton() {
        std::cout << "Singleton instance destroyed." << std::endl;
    }

    // 静态指针,用于存储单例实例
    static Singleton* instance_;
};

// 初始化静态指针为nullptr
Singleton* Singleton::instance_ = nullptr;

int main() {
    // 获取单例实例并调用其成员函数
    Singleton* singleton = Singleton::getInstance();
    singleton->doSomething();

    // 注意:在这个例子中,我们没有手动调用cleanup函数,因为程序结束时会自动销毁全局/静态对象。
    // 然而,在实际应用中,如果单例持有需要手动释放的资源(如文件句柄、网络连接等),
    // 则应该提供一个清理机制(如cleanup函数)来确保资源被正确释放。
    // 另外,由于这个示例是线程不安全的,因此在实际多线程环境中使用时需要额外的同步机制。

    return 0;
}

线程安全的懒汉式单例

为了使懒汉式单例在多线程环境中安全,我们需要添加同步机制。以下是一个使用互斥锁(std::mutex)来实现线程安全的懒汉式单例的示例:

#include <iostream>
#include <mutex>

class Singleton {
public:
    // ...(与上面相同)

    // 线程安全的getInstance方法
    static Singleton* getInstance() {
        std::lock_guard<std::mutex> lock(mutex_); // 使用RAII风格的锁来管理互斥锁的生命周期
        if (instance_ == nullptr) {
            instance_ = new Singleton();
        }
        return instance_;
    }

    // ...(与上面相同)

private:
    // ...(与上面相同)

    // 静态互斥锁,用于保护单例实例的创建过程
    static std::mutex mutex_;

    // 静态指针,用于存储单例实例
    static Singleton* instance_;
};

// 初始化静态成员变量
std::mutex Singleton::mutex_;
Singleton* Singleton::instance_ = nullptr;

// ...(main函数与上面相同)

在这个线程安全的版本中,我们添加了一个静态的std::mutex成员变量mutex_,并在getInstance方法中使用std::lock_guard来管理锁的生命周期。这样,即使多个线程同时调用getInstance方法,也只有一个线程能够创建实例,从而保证了单例性。 

4.1.3 双重检查锁定(DCL)

  • 结合了懒汉式和同步锁的优点,既实现了延迟加载,又保证了线程安全。
  • 但实现起来相对复杂,需要谨慎处理。
  • 应用场景:适用于高并发环境,需要确保线程安全且希望减少同步锁开销的情况。
  • 优点:结合了懒汉式和同步锁的优点,既实现了延迟加载,又减少了同步锁的开销。
  • 缺点:实现复杂,需要谨慎处理以避免潜在的问题,如“伪共享”等。

 双重检查锁定(Double-Checked Locking)是一种优化技术,用于在多线程环境中实现懒汉式单例模式,同时减少获取锁的开销。在C++11及更高版本中,我们可以利用std::atomicstd::mutex来实现线程安全的双重检查锁定。

#include <iostream>
#include <atomic>
#include <mutex>

class Singleton {
public:
    // 禁止拷贝构造函数和赋值操作符
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

    // 提供一个全局访问点来获取单例实例
    static Singleton* getInstance() {
        if (instance_ == nullptr) { // 第一次检查,无需加锁
            std::lock_guard<std::mutex> lock(mutex_); // 加锁进行第二次检查
            if (instance_ == nullptr) { // 第二次检查,确保实例未被其他线程创建
                instance_ = new Singleton();
            }
        }
        return instance_;
    }

    // 其他成员函数
    void doSomething() {
        std::cout << "Doing something in the Singleton instance." << std::endl;
    }

    // 提供一个清理函数(通常不需要手动调用,除非有特殊需求)
    static void cleanup() {
        delete instance_;
        instance_ = nullptr;
    }

private:
    // 私有构造函数,防止外部直接创建实例
    Singleton() {
        std::cout << "Singleton instance created." << std::endl;
    }

    // 私有析构函数,防止外部删除实例
    ~Singleton() {
        std::cout << "Singleton instance destroyed." << std::endl;
    }

    // 静态原子指针,用于存储单例实例,保证线程安全
    static std::atomic<Singleton*> instance_;

    // 静态互斥锁,用于保护单例实例的创建过程
    static std::mutex mutex_;
};

// 初始化静态成员变量
std::atomic<Singleton*> Singleton::instance_(nullptr);
std::mutex Singleton::mutex_;

int main() {
    // 获取单例实例并调用其成员函数
    Singleton* singleton = Singleton::getInstance();
    singleton->doSomething();

    // 注意:cleanup函数通常不需要手动调用,因为程序结束时会自动销毁全局/静态对象。
    // 然而,如果单例持有需要手动释放的资源(如文件句柄、网络连接等),
    // 则应该提供一个清理机制(如cleanup函数)来确保资源被正确释放。
    // 在这个例子中,我们没有手动调用cleanup函数。

    return 0;
}

 在这个示例中,instance_是一个std::atomic<Singleton*>类型的静态成员变量,它保证了指针赋值的原子性。第一次检查instance_ == nullptr时不需要加锁,这减少了获取锁的开销。如果第一次检查发现实例尚未创建,则进入加锁区域进行第二次检查,以确保实例未被其他线程创建。如果实例确实尚未创建,则创建它。

需要注意的是,虽然双重检查锁定可以减少获取锁的开销,但它也增加了代码的复杂性。此外,C++11及更高版本中的std::call_oncestd::once_flag提供了一种更简单且线程安全的方式来初始化静态局部变量,这可能是实现单例模式的更好选择。然而,了解双重检查锁定的实现原理仍然是有价值的,因为它展示了如何在多线程环境中进行精细的同步控制。

4.1.4 静态内部类(C++11及以后)

  • 利用C++11的局部静态变量初始化特性,可以实现既线程安全又延迟加载的单例模式。
  • 这种方法实现简单且高效,是C++11及以后版本的推荐实现方式。
  • 应用场景:适用于C++11及以后版本,希望实现线程安全且简洁的单例模式。
  • 优点:利用C++11的局部静态变量初始化特性,既线程安全又实现了延迟加载,且实现简洁。
  • 缺点:需要C++11及以上版本的支持。

4.1.5 使用std::once_flagstd::call_once(C++11及以上)

  • 确保某个代码块只被执行一次,从而实现单例模式的线程安全和延迟加载。
  • 依赖于C++11及以上版本的特性。
  • 应用场景:适用于C++11及以上版本,希望确保某个代码块只被执行一次的情况。
  • 优点:利用C++11的标准库特性,确保线程安全且易于理解。
  • 缺点:需要C++11及以上版本的支持。

4.1.6 枚举单例

  • 应用场景:适用于C++11及以上版本,希望防止外部通过delete操作符删除单例实例的情况。
  • 优点:利用枚举类型的特性,确保单例实例在全局范围内唯一且不可被删除。
  • 缺点:需要C++11及以上版本的支持,且实现方式相对不常见。

4.1.7 线程局部存储

  • 应用场景:在多线程环境下,每个线程都需要一个独立的实例,但这些实例之间又需要保持某种独立性(不是全局单例)。
  • 优点:为每个线程提供独立的实例,避免了线程间的数据竞争。
  • 缺点:虽然严格意义上不是单例模式,但在某些多线程场景下有其独特的用途。

4.2 单例模式分类的区别

  1. 饿汉式
    • 在类加载时就完成了实例的初始化。
    • 线程安全,因为实例在类加载时已经创建,无需担心多线程访问问题。
    • 无法实现延迟加载,如果实例在程序执行过程中并不需要,则会造成资源浪费。
  2. 懒汉式
    • 在第一次调用getInstance方法时才创建实例。
    • 可以实现延迟加载,节省资源。
    • 需要处理线程安全问题,如果多个线程同时调用getInstance方法,可能会导致多个实例被创建。
  3. 双重检查锁定(DCL)
    • 结合了懒汉式和同步锁的优点。
    • 使用双重检查来减少同步锁的开销,并确保线程安全。
    • 实现相对复杂,需要谨慎处理以避免潜在的问题。
  4. 静态内部类(Meyers' Singleton):
    • 利用C++11的局部静态变量初始化特性。
    • 既线程安全又实现了延迟加载。
    • 实现简单且高效,是C++11及以后版本的推荐实现方式。
  5. 使用std::call_oncestd::once_flag
    • 依赖于C++11及以上版本的特性。
    • 确保某个代码块只被执行一次,从而实现单例模式的线程安全和延迟加载。
    • 相比双重检查锁定,这种方式更加简洁且易于理解。

 

标签:std,Singleton,模式,实例,线程,C++,单例,设计模式
From: https://blog.csdn.net/a8039974/article/details/143804756

相关文章

  • 一文带你了解防火墙的三种工作模式:路由模式、透明模式(网桥)、混合模式。网络安全零基础
    防火墙作为网络安全的核心设备之一,扮演着至关重要的角色。它不仅能够有效防御外部网络的攻击,还能保护内部网络的安全。在如今复杂多样的网络环境下,防火墙的部署和工作模式直接影响着网络安全策略的实施效果。防火墙通常可以工作在三种模式下:路由模式、透明模式(网桥模式)以及......
  • 设计模式已经过时了?再也不用学了?
    设计模式是高级软件开发工程师的必备技能之一。虽然设计模式本身并不是解决所有问题的万能钥匙,但掌握设计模式可以帮助开发者在以下方面显著提升能力和效率:1.设计模式的意义设计模式是一种总结了软件开发中的常见问题和解决方案的经验集合。通过学习和使用设计模式,开发......
  • Jarvis March算法详解及Python实现(附设计模式案例)
    目录JarvisMarch算法详解及Python实现(附设计模式案例)第一部分:JarvisMarch算法概述与原理1.1什么是JarvisMarch算法?1.2算法原理1.3算法流程1.4时间复杂度第二部分:JarvisMarch算法的Python实现(面向对象设计)2.1面向对象设计2.2代码实现2.3代......
  • 设计模式学习笔记之七大原则
    设计模式的七大原则开闭原则(OpenClosedPrinciple,OCP)单一职责原则(SingleResponsibilityPrinciple,SRP)里氏代换原则(LiskovSubstitutionPrinciple,LSP)依赖倒转原则(DependencyInversionPrinciple,DIP)接口隔离原则(InterfaceSegregationPrinciple,ISP)合成/聚合复用原则(Co......
  • 跨企业、跨区域的 FMEA 数据共享和协作模式的探索
    【大家好,我是唐Sun,唐Sun的唐,唐Sun的Sun。】在当今全球化和高度互联的商业环境中,跨企业、跨区域的FMEA(失效模式及后果分析)数据共享和协作模式具有重要意义。首先,建立统一的数据标准和格式是实现有效共享的基础。不同企业和区域可能采用各自独特的FMEA记录方式和术语,这会导......
  • (mongodb副本集) PSA模式添加、修改节点
    (mongodb副本集)PSA模式添加、修改节点PSA模式介绍PSA模式(Primary-Secondary-Arbiter)是MongoDB复制集中的一种架构配置。在这种模式下,复制集由一个主节点(PRIMARY)、一个从节点(SECONDARY)和一个仲裁者节点(ARBITER)组成。在MongoDB的复制集中,仲裁者节点(Arbiter)是一种特殊类型的节点,......
  • (mongodb副本集) PSA模式添加、修改节点
    (mongodb副本集)PSA模式添加、修改节点PSA模式介绍PSA模式(Primary-Secondary-Arbiter)是MongoDB复制集中的一种架构配置。在这种模式下,复制集由一个主节点(PRIMARY)、一个从节点(SECONDARY)和一个仲裁者节点(ARBITER)组成。在MongoDB的复制集中,仲裁者节点(Arbiter)是一种特殊类型的节点,......
  • (mongodb副本集) PSA模式添加、修改节点
    (mongodb副本集)PSA模式添加、修改节点PSA模式介绍PSA模式(Primary-Secondary-Arbiter)是MongoDB复制集中的一种架构配置。在这种模式下,复制集由一个主节点(PRIMARY)、一个从节点(SECONDARY)和一个仲裁者节点(ARBITER)组成。在MongoDB的复制集中,仲裁者节点(Arbiter)是一种特殊类型的节点,......
  • 【提高篇】3.3 GPIO(三,工作模式详解 上)
    目录一,工作模式介绍二,输入浮空三,输入上拉一,工作模式介绍GPIO有八种工作模式,参考下面列表,我们先有一个简单的认识。二,输入浮空在输入浮空模式下,上拉/下拉电阻为断开状态,施密特触发器打开,输出被禁止。输入浮空模式下,IO口的电平完全是由外部电路决定。如果IO引脚没有......
  • instanceof 的模式匹配(一)
    前言相信你在Java编程中用到过如下的操作://调用上游接口.返回结果objObjectobj=getObj();//判断返回值是不是字符串if(objinstanceofString){Stringobjstr=(String)obj;//dosomethingwithobjstr}以上这种instanceof-and-cast惯用语的......