10.单例模式 (Singleton Pattern)

news/2025/2/6 17:17:36 标签: 单例模式

单例模式的定义

单例模式(Singleton Pattern) 是一种创建型设计模式,确保一个类在整个程序生命周期中只能有一个实例,并提供一个全局访问点。

特点:

  • 唯一性:保证系统中某个类只能有一个实例。
  • 全局访问点:提供全局的静态方法来访问这个唯一实例。
  • 懒加载(Lazy Initialization):实例只会在第一次访问时创建,节省资源。

适用场景

单例模式常用于以下场景:

  • 管理共享资源(如数据库连接池、线程池、日志管理器)。
  • 全局状态管理(如游戏引擎中的配置管理器)。
  • 设备驱动(如打印机管理类,确保同时只有一个任务访问)。
  • 缓存(如 DNS 解析缓存)。
  • 配置管理(如应用程序的配置文件管理)。

在软件系统中,经常有这样一些特殊类,必须保证它们在系统中只存在一个实例,才能确保他们的逻辑正确性,以及良好的效率。

如何绕过常规的构造器,提供一种机制来保证一个类只有一个实例?

class Singleton{
private:
    Singleton();
    Singleton(const Singleton& other);
public:
    static Singleton* getInstance();
    static Singleton* m_instance;
};

Singleton* Singleton::m_instance=nullptr;

//线程非安全版本
Singleton* Singleton::getInstance() {
    if (m_instance == nullptr) {
        m_instance = new Singleton();
    }
    return m_instance;
}

上述这种实现在多线程的情况下并不安全。解决这种情况可以加锁,但是锁的代价太大,因为每次访问都会被锁。

//线程安全版本,但锁的代价过高
Singleton* Singleton::getInstance() {
    Lock lock;
    if (m_instance == nullptr) {
        m_instance = new Singleton();
    }
    return m_instance;
}

加双检查锁(锁前锁后双检查)就可以避免上述这个问题:

//双检查锁,但由于内存读写reorder不安全
Singleton* Singleton::getInstance() {
    
    if(m_instance==nullptr){
        Lock lock;
        if (m_instance == nullptr) {
            m_instance = new Singleton();
        }
    }
    return m_instance;
}

上述这种实现仍然会存在一些问题。线程是在指令层次抢时间片,也就是 m_instance = new Singleton(); 这一行代码可能会先分配内存,然后把内存地址给 m_instance,然后再执行构造器。这样的话就会存在问题,当线程1还没有执行构造函数,但是已经有了地址 m_instance 之后,另外一个线程进来发现 m_instance 不是nullptr就直接返回了这个地址。但是这个地址是不能用的,因为还没有调用构造器。

经典的 Singleton 实现(C++11 之前)

在 C++11 之前,实现单例模式时,需要考虑线程安全和双重检查锁定(DCLP, Double-Checked Locking Pattern):

class Singleton {
private:
    static Singleton* instance;  // 静态指针,指向唯一实例
    static std::mutex m_mutex;   // 互斥锁,确保线程安全
    Singleton() {}               // 构造函数私有,防止外部创建对象
    ~Singleton() {}
public:
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

    static Singleton* getInstance() {
        if (instance == nullptr) {  // 第一次检查(避免不必要加锁)
            std::lock_guard<std::mutex> lock(m_mutex);
            if (instance == nullptr) {  // 第二次检查,确保不会创建多个实例
                instance = new Singleton();
            }
        }
        return instance;
    }
};

// 初始化静态成员
Singleton* Singleton::instance = nullptr;
std::mutex Singleton::m_mutex;

缺点:

  • 需要手动管理 instance 的内存释放,可能导致内存泄漏。
  • 在多线程环境下,需要加锁,可能会影响性能。

C++11 之后的跨平台实现

C++11 提供了 std::atomic 和 std::mutex,使得单例模式可以更加高效和安全。

#include <atomic>
#include <mutex>

class Singleton {
private:
    static std::atomic<Singleton*> m_instance; // 使用 atomic 保证实例安全
    static std::mutex m_mutex;                 // 互斥锁,用于加锁
    Singleton() {}  // 构造函数私有
    ~Singleton() {}

public:
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

    static Singleton* getInstance() {
        Singleton* tmp = m_instance.load(std::memory_order_relaxed);  // 先读取原子变量
        std::atomic_thread_fence(std::memory_order_acquire);  // 获取内存屏障,保证可见性
        if (tmp == nullptr) {  // 第一次检查
            std::lock_guard<std::mutex> lock(m_mutex);
            tmp = m_instance.load(std::memory_order_relaxed);
            if (tmp == nullptr) {  // 第二次检查
                tmp = new Singleton;
                std::atomic_thread_fence(std::memory_order_release);  // 释放屏障,确保构造完成
                m_instance.store(tmp, std::memory_order_relaxed);
            }
        }
        return tmp;
    }
};

// 初始化静态成员
std::atomic<Singleton*> Singleton::m_instance;
std::mutex Singleton::m_mutex;

实现步骤
1. 使用 std::atomic 存储 m_instance,确保在多线程环境下安全访问单例实例。

2. 使用 std::mutex 进行加锁,保证线程安全。

3. 双重检查锁定(DCLP):
3.1 第一次检查 if (tmp == nullptr),避免不必要的加锁。
3.2 第二次检查 if (tmp == nullptr),防止多个线程同时进入加锁区域导致创建多个实例。

4.使用 std::atomic_thread_fence:
4.1 memory_order_acquire 确保 tmp 读取到最新的数据。
4.2 memory_order_release 确保 m_instance 赋值完成后,其他线程能看到完整对象
  1. 更加现代的实现方式(C++11 及之后)
    C++11 提供了更简单、安全的 std::call_once,可以替代 std::mutex 和 std::atomic。
#include <mutex>

class Singleton {
private:
    Singleton() {} // 私有构造函数
    ~Singleton() {}
    static std::once_flag initFlag;  // 用于确保初始化只执行一次
    static Singleton* instance;

public:
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

    static Singleton* getInstance() {
        std::call_once(initFlag, []() { instance = new Singleton(); }); // 只执行一次
        return instance;
    }
};

// 初始化静态成员
Singleton* Singleton::instance = nullptr;
std::once_flag Singleton::initFlag;

优势:

  • std::call_once 比锁更高效,因为它只在 getInstance() 第一次调用时执行初始化。
  • 避免了 std::mutex 的加锁开销,减少性能损耗。
  1. C++11 线程安全的懒汉式(推荐)
    如果 C++11 及以上,可以直接使用局部静态变量(Meyers’ Singleton)。
class Singleton {
private:
    Singleton() {}
    ~Singleton() {}
public:
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

    static Singleton& getInstance() {
        static Singleton instance; // 线程安全的懒汉式单例
        return instance;
    }
};

优势:

  • 线程安全:C++11 标准保证了局部静态变量的初始化是线程安全的。
  • 简单:没有 std::mutex 或 std::atomic,代码更易读。
  • 生命周期管理:instance 会在程序结束时自动销毁。

典型调用方式

int main() {
    Singleton* instance1 = Singleton::getInstance();
    Singleton* instance2 = Singleton::getInstance();

    if (instance1 == instance2) {
        std::cout << "两个实例是相同的,单例模式成功!" << std::endl;
    }
    return 0;
}

总结

方式线程安全实现复杂度适用场景
传统懒汉式× 否简单仅限单线程
DCLP(双重检查锁定)√ 是复杂C++11 之前的多线程
std::atomic√ 是较复杂需要高性能
std::call_once√ 是简单现代 C++ 推荐
局部静态变量√ 是最简单C++11 推荐

要点总结

Singleton模式中的实力构造器可以设置为protected以允许子类派生。

Singleton模式一般不要支持拷贝构造函数和Clone接口,因为这有可能导致多个对象实例,与Singleton模式的初衷违背。


http://www.niftyadmin.cn/n/5843205.html

相关文章

【算法篇】贪心算法

目录 贪心算法 贪心算法实际应用 一&#xff0c;零钱找回问题 二&#xff0c;活动选择问题 三&#xff0c;分数背包问题 将数组和减半的最小操作次数 最大数 贪心算法 贪心算法&#xff0c;是一种在每一步选择中都采取当前状态下的最优策略&#xff0c;期望得到全局最优…

十。svm运用

import numpy as np import matplotlib.pyplot as plt from sklearn import datasets from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler from sklearn.svm import SVC # 创建自定义数据集 np.random.seed(42) X…

七。自定义数据集 使用tensorflow框架实现逻辑回归并保存模型,然后保存模型后再加载模型进行预测

import tensorflow as tf import numpy as np # 自定义数据集类 class CustomDataset(tf.data.Dataset): def __init__(self, x_data, y_data): self.x_data tf.convert_to_tensor(x_data, dtypetf.float32) self.y_data tf.convert_to_tensor(y_data, …

无人机测绘技术,为行业开启解决方案新篇章!

随着国土建设的专业化程度不断提升和工作负荷的持续加重&#xff0c;传统测绘方法的局限性也日益显露。它们不仅容易受环境及恶劣气候的制约&#xff0c;还面临着人力资源短缺的挑战&#xff0c;难以适应当前高度专业化的需求。而无人机测绘技术凭借其高度的灵活性和强大的适应…

网络安全--边界安全-防火墙

随着安全技术的发展&#xff0c;无论是黑客的攻击能力&#xff0c;还是安全人员的防御技术都上升到了一个新的层次&#xff0c;而且安全威胁越来越大&#xff0c;越来越隐蔽&#xff0c;本篇就边界安全另一利器----防火墙进行讲述。见到过不少厂家的防火墙设备&#xff0c;有些…

堆(Heap)的原理与C++实现

1. 什么是堆&#xff1f; 堆&#xff08;Heap&#xff09;是一种特殊的树形数据结构&#xff0c;通常用于实现优先队列。堆可以分为两种类型&#xff1a; 最大堆&#xff08;Max Heap&#xff09;&#xff1a;每个节点的值都大于或等于其子节点的值。最小堆&#xff08;Min H…

【数据结构】循环链表

循环链表 单链表局限性单向循环链表判断链表是否有环思路code 找到链表入口思路代码结构与逻辑 code 单链表局限性 单链表作为一种基本的数据结构&#xff0c;虽然在很多场景下都非常有用&#xff0c;但它也存在一些局限性&#xff1a; 单向访问&#xff1a;由于每个节点仅包含…

VMware下Linux和macOS遇到的一些问题总结

1.解决.NET程序通过网盘传到Linux和macOS不能运行的问题 这是文件权限的问题。当你通过U盘将文件传输到虚拟机的macOS和Linux系统时&#xff0c;文件的权限和所有权可能得到了保留或正确设置。但如果你通过网盘上传&#xff0c;文件的权限或所有权可能没有正确设置&#xff0c…