线程安全的单例模式

发布于:2025-07-29 ⋅ 阅读:(15) ⋅ 点赞:(0)

什么是单例模式

单例模式作为设计模式中极具代表性的经典范式,不仅是日常开发里频繁使用的创建型模式,更是各类技术考核中的高频考点。其核心设计目标在于严格保证一个类在整个系统生命周期内仅存在唯一实例,同时提供一个简洁、统一的全局访问入口,让系统中的任何模块都能便捷地获取并操作这个实例。

这种模式通过巧妙的构造机制规避了类的重复实例化,既节省了不必要的资源消耗,又确保了全局状态的一致性,在配置管理、工具类封装、线程池等场景中展现出不可替代的价值。

什么是设计模型

IT行业这么火,涌入的人很多。俗话说林子大了啥鸟都有,以至于大佬和菜鸡们两极分化的越来越严重。为了让菜鸡们不太拖大佬的后腿,于是大佬们针对一些经典的常见的场景,给定了一些对应的解决方案。这个就是设计模式。

单例模式的特点

某些类,只应该具有一个对象(实例),就称之为单例。

例如一个男人只能有一个媳妇。

在很多服务器开发场景中,经常需要让服务器加载很多的数据 (上百G) 到内存中。此时往往要用一个单例的类来管理这些数据。

单例模式的实现方式

单例模式的实现方式分为两种

  1. 饿汉实现方式
  2. 懒汉实现方式

我们拿洗碗的例子来做比较

吃完饭,立刻洗碗,这种就是饿汉方式。因此下一顿吃的时候可以立刻拿着碗就能吃饭。

吃完饭,先把碗放下, 然后下一顿饭用到这个碗了再洗碗,就是懒汉方式。

懒汉方式最核心的思想是“延时加载”,从而能够优化服务器的启动速度。

比方说系统中的malloc,当调用malloc后,是仅系统允许你可以去访问,只有当你真正的去访问的时候,才会发生缺页中断,此时才会真正的申请到空间。其本质就类似于懒汉。

饿汉方式实现单例模式

template <typename T>
class Singleton 
{
    static T data;
public:
    static T* GetInstance() 
    {
        return &data;
    }
};
  • 由于我们在类成员变量前加了static,使其变为了静态成员变量,在程序启动时初始化。而不是在实例化时才创建。
  • 我们的GetInstance被static修饰,其函数属于类,而非对象,不能访问非静态成员。
  • 除此之外,我们只有通过Sigleton这个包装类才可以使用T对象,则一个进程中只有一个T对象的实例。
  • GetInstance() 只是返回已经存在的实例,它既没有检查 data 是否为空,也没有在第一次调用时创建 data,而是直接返回 &data。这说明 data 的初始化是 提前完成的,而不是 按需加载

懒汉方式实现单例模式

template <typename T>
class Singleton 
{
    static T* inst;
public:
    static T* GetInstance() 
    {
        if (inst == NULL) 
        {
            inst = new T();
        } 
        return inst;
    }
};
  • 同样,由于我们在类成员变量前加了static,使其变为了静态成员变量,在程序启动时初始化。而不是在实例化时才创建。
  • 同样,我们的GetInstance被static修饰,其函数属于类,而非对象,不能访问非静态成员。
  • 除此之外,我们只有通过Sigleton这个包装类才可以使用T对象,则一个进程中只有一个T对象的实例。
  • 当第一次调用GetInstance时,此时就会发现inst还未进行初始化,此时第一次就会进行初始化。其为动态分配,按需加载。

但这里存在一个严重的问题:线程不安全。

第一次调用 GetInstance 的时候,如果两个线程同时调用,可能会创建出两份 T 对象的实例。但是后续再次调用,就没有问题了。虽然说是线程不安全,但此时还是符合单例模式的,只是进行了两次或多次初始化。会导致,导致内存泄漏与数据二义性。

那么如何改进呢?主要思想还是进行加锁。

懒汉方式实现单例模式(线程安全版本)

// 懒汉模式, 线程安全
template <typename T>
class Singleton 
{
    volatile static T* inst; // 需要设置 volatile 关键字, 否则可能被编译器优化.
    static std::mutex lock;
public:
    static T* GetInstance() 
    {
        if (inst == NULL) 
        { 
            // 双重判定空指针, 降低锁冲突的概率, 提高性能.
            lock.lock(); // 使用互斥锁, 保证多线程情况下也只调用一次 new.
            if (inst == NULL) 
            {
                inst = new T();
            }  
            lock.unlock();
        } 
        return inst;
    }
};

注意事项:加锁解锁的位置,双重if判断,避免不必要的锁竞争。volatile关键字防止过度优化。

除了此,对于单例模式,还需要将拷贝函数删除了,其主要也是保证单个进程仅可初始化一个实例对象。采用c++11的模式删除。

Singleton(const Singleton&) = delete;


网站公告

今日签到

点亮在社区的每一天
去签到