设计模式之单例模式

单例模式

(Singleton Pattern)是一种创建型设计模式,限制一个类只能有一个实例化对象,并提供一个全局访问方式。
  • 优点
不适用于频繁变化的对象、扩展困难、可能违背单一职责原则等
  • 使用场景
    • 游戏引擎核心管理器: 确保游戏引擎的核心功能只有一个实例,提供全局访问点。
    • 资源管理器: 管理游戏中的资源,如纹理、声音等,确保资源的唯一性和高效访问。
    • 全局配置管理:系统中的配置信息通常需要被多个组件共享,使用单例模式可以确保所有的组件访问的是同一个配置对象。
    • 线程池管理:线程池是执行后台任务的理想选择,单例模式可以确保整个应用中只存在一个线程池实例,避免资源浪费。
    • 数据库连接池:数据库连接是一种宝贵的资源,使用单例模式可以有效地管理这些连接,确保不会创建过多的连接实例。
    • 日志记录器:日志记录器通常需要在整个应用中共享,以便于跟踪和记录日志信息,单例模式可以确保所有日志调用都指向同一个日志记录器实例。
    • Web应用的配置对象:Web应用中的配置对象,如Spring的ApplicationContext,通常使用单例模式来确保整个应用共享同一个配置上下文。
    • 操作系统的特定服务:例如,Windows的Task Manager(任务管理器)和Recycle Bin

单例模式的实现

  • 懒汉模式
懒汉式单例模式指的是在第一次访问时才创建唯一实例。这种实现方式在实例创建开销较大或者实例使用不频繁时,可以减少不必要的资源开销。但在多线程环境下,需要使用同步锁来确保 线程安全
  • 饿汉模式
饿汉式单例模式指的是在类加载时就创建唯一实例。这种实现方式能保证线程安全,因为类加载时的操作是线程安全的。但是,由于实例在类加载时就创建,无论是否需要使用都会占用资源,可能导致资源浪费。
个人习惯是推荐用饿汉模式,整体简单,代码好理解. 已经用单例了还考虑资源占用问题, 对于服务端开发而言没意义.因为这点资源开服期间能加载是比较好的,避免中途加载耗费大量cpu.

单例模式代码实现细节

  • 互斥锁
懒汉式单例模式即在第一次获取单例时才创建单例
#include <iostream>
using namespace std;

class Singleton{
private:
    // 构造函数要设置为私有的,防止外部直接调用创建实例
    Singleton() {}
    // 删除拷贝构造函数,防止拷贝实例
    Singleton(const Singleton&) = delete;
    // 删除赋值操作符,防止复制实例
    Singleton& operator =(const Singleton&) = delete;

    // 静态成员变量,保存唯一实例
    static Singleton *m_instance;
    static mutex m_mutex;
public:
    static Singleton *getInstance() {
        // 为了线程安全加互斥锁,但是每次访问都得加锁,开销大
        lock_guard<mutex> lock(m_mutex); // 加锁
        if (m_instance == nullptr) {
            m_instance = new Singleton();
        }
        return m_instance;
    }
};

Singleton* Singleton::m_instance = nullptr;
mutex Singleton::m_mutex;

为了线程安全加互斥锁,导致是每次访问都得加锁开销大,考虑采用双重检查锁定
  • 双重检查锁定
 static Singleton *getInstance() {
    if (m_instance == nullptr) { // 第一次检查,如果实例已经被创建,则不用加锁
        lock_guard<mutex> lock(m_mutex);
        if (m_instance == nullptr) { // 第二次检查
            m_instance = new Singleton();
        }
    }
    return m_instance;
}
这行代码在实际操作中分为三步:
  • ① 分配内存
  • ② 调用构造函数初始化对象
  • ③ 将内存地址赋值给m_instance
指令重排就可能会导致步骤③提前到步骤②之前进行,那么当运行完步骤③之后,m_instance就已经是非空了,但此时步骤②还没执行,如果此时有另外一个线程来获取实例,就会获取到一个未完全初始化的实例,从而导致程序行为异常,道阻且艰,让我继续优化
  • 内存屏障
#include <iostream>
#include <mutex>
#include <atomic>

using namespace std;

class Singleton{
private:
    // 构造函数要设置为私有的,防止外部直接调用创建实例
    Singleton() {}
    // 删除拷贝构造函数,防止拷贝实例
    Singleton(const Singleton&) = delete;
    // 删除赋值操作符,防止复制实例
    Singleton& operator =(const Singleton&) = delete;

    // 静态成员变量,保存唯一实例
    static atomic<Singleton *> m_instance; // 声明为原子类型
    static mutex m_mutex;
public:
    static Singleton *GetInstance() {
        Singleton *tmp = m_instance.load(memory_order_acquire);
        if (tmp == nullptr) {
            lock_guard<mutex> lock(m_mutex);
            tmp = m_instance.load(memory_order_relaxed);
            if (tmp == nullptr) {
                tmp = new Singleton();
                m_instance.store(tmp, memory_order_release);
            }
        }
        return m_instance;
    }
};

Singleton* Singleton::m_instance = nullptr;
mutex Singleton::m_mutex;
        首先,将m_instance声明为 atomic原子变量,确保对它的读写操作都是原子操作。
        在第一次检查时使用 memory_order_acquire,保证在当前线程读取某个原子变量之前,所有其他线程对该变量的写操作都已经完成,且对其他线程可见,意思就是说:所获取到的值一定是写入之后的结构,保证了读写顺序。
        赋值的时候使用 memory_order_release,确保之前的写操作不会被重排到当前操作之后,意思就是说,在tmp赋值给m_instance之前,tmp一定是完成了构造函数初始化的,这样就确保m_instance中一定是一个已完成构造的完整实例。
(C++11之前的也有对应的内存屏障函数可用,也可解决该问题)
看到这里麻了没?以为简简单单的单例模式,竟然有这么多坑,竟然需要这么长的一段代码,别急,图灵大佬们早就替我们想到了更好的解决方式,接着往下看。
  • 使用局部静态变量
在C++11及其以上版本,支持使用局部静态变量的线程安全初始化,可以利用这一特性实现线程安全的单例模式。
#include <iostream>

using namespace std;

class Singleton{
private:
    // 构造函数要设置为私有的,防止外部直接调用创建实例
    Singleton() {}
    // 删除拷贝构造函数,防止拷贝实例
    Singleton(const Singleton&) = delete;
    // 删除赋值操作符,防止复制实例
    Singleton& operator =(const Singleton&) = delete;

public:
    static Singleton &getInstance() {
        static Singleton m_instance; // 局部静态变量,线程安全
        return m_instance;
    }
};
那有人要问了,为啥这个局部静态变量就一定是线程安全的?
其实是C++11标准中明确规定了局部静态变量的初始化必须保证线程安全,编译器会在底层实现中自动加入现成同步机制,确保多个线程同时调用时,局部静态变量只会被初始化一次,原理和我们上面几种方法的代码差不多,只是不由我们自己实现了而已,该有的性能损耗还是有的。
  • call_once函数模板
C++11中还提供了一个std::call_once的函数模板,可以确保某个函数在多个线程中只被调用一次,原型如下:
template<typename _Callable, typename... _Args> 
void call_once(std::once_flag& flag, _Callable&& func, _Args&&...args);
我们使用call_once对我们的代码再次进行改动:
#include <iostream>
#include <mutex>

using namespace std;

class Singleton {
private:
    // 构造函数要设置为私有的,防止外部直接调用创建实例
    Singleton() {}
    // 删除拷贝构造函数,防止拷贝实例
    Singleton(const Singleton&) = delete;
    // 删除赋值操作符,防止复制实例
    Singleton& operator =(const Singleton&) = delete;

    static Singleton *m_instance;
    static once_flag m_flag;

    void createInstance() {
        m_instance = new Singleton();
    }

public:
    static Singleton *getInstance() {
        call_once(m_flag, createInstance); // 确保只调用一次
        return m_instance;
    }
};

Singleton *Singleton::m_instance = nullptr;
once_flag Singleton::m_flag;
参考:
[1] https://zhuanlan.zhihu.com/p/22489545167 这篇对单例模式讲述正确性较高
[2] https://zhuanlan.zhihu.com/p/696460150 讲述单例模式的应用场景
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值