Logic / 四:单例模式--极简教程

Created Tue, 23 Sep 2025 01:28:56 +0800 Modified Wed, 24 Sep 2025 01:02:44 +0800

单例模式

类图如下

饿汉 懒汉
特点 线程安全,实现简单 需要时才创建,节省资源
缺点 可能浪费资源 需要额外同步机制

饿汉式

EagerSingleton.h

class EagerSingleton {
public:
    static EagerSingleton* getInstance();
    ///如果你不显式删除拷贝构造和赋值运算符,C++ 编译器会自动生成它们
    EagerSingleton(const EagerSingleton&)=delete;
    EagerSingleton& operator=(const EagerSingleton&)=delete;
    // 显式删除移动操作,当显式声明或delete了复制操作后,
    // 编译器不会自动创建移动操作,因此不用显式删除移动操作也可以
    EagerSingleton(EagerSingleton&&) = delete;
    EagerSingleton& operator=(EagerSingleton&&) = delete;
private:
    EagerSingleton()=default;
    ~EagerSingleton()=default;
    static EagerSingleton *m_instance;
};

EagerSingleton.cpp

// 类外定义并初始化,这里实际分配内存并调用构造函数
EagerSingleton* EagerSingleton::m_instance = new EagerSingleton(); 
EagerSingleton* EagerSingleton::getInstance()
{
    return m_instance;
}

适用场景

  • 场景1:启动时必须初始化的核心组件,如日志组件
  • 场景2:配置相关的单例
  • 场景3: 初始化环境变量

关于静态变量的补充说明

在C++中,静态变量(包括全局变量和类的静态成员变量)的初始化分为两种:

静态初始化:在程序加载时完成,通常用于初始化内置类型(如int、指针等)的常量表达式。

动态初始化:对于需要执行构造函数等复杂初始化的类型,则需要进行动态初始化。

饿汉式的单例通常属于动态初始化。C++标准规定,动态初始化的非局部变量(即全局变量和静态成员变量)的初始化发生在main函数执行之前,
但是同一个编译单元内的变量初始化顺序是按照定义顺序进行的,而不同编译单元之间的初始化顺序是未定义的。

所以,饿汉式单例的初始化发生在main函数之前,但具体时间点由编译器决定,并且跨编译单元的初始化顺序是不确定的。

解决懒汉式中的线程问题

实现一:简单加锁

class Singleton {
private:
    static Singleton* m_instance;
    static std::mutex m_mutex;

    Singleton() {} // 私有构造函数

public:
    static Singleton* getInstance() {
        std::lock_guard<std::mutex> lock(m_mutex); // 方法级锁
        if (m_instance == nullptr) {
            m_instance = new Singleton();
        }
        return m_instance;
    }
};

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

问题:锁只有在实例未建立的时候才是必须的,一旦实例创建成功,后续的锁操作(锁的获取和释放)纯粹是性能开销。在高并发场景下,这会成为瓶颈。

实现二:双重检查锁定机制

双重检查锁定机制的核心思想是:加锁之前和之后各检查一次实例指针

  • 看似正确实际错误的实现
// 警告:此代码有严重问题,不能使用!
class Singleton {
private:
    static Singleton* instance;
    static std::mutex mutex_;

    Singleton() {}

public:
    static Singleton* getInstance() {
        // 第一次检查 (无锁)
        if (instance == nullptr) {
            // 加锁
            std::lock_guard<std::mutex> lock(mutex_);
            // 第二次检查 (有锁)
            if (instance == nullptr) {
                instance = new Singleton(); // 问题所在!
            }
        }
        return instance;
    }
};

问题根源:指令重排序

问题出在 instance = new Singleton(); 这行代码。它并非一个原子操作,通常包含三个步骤:

Step 1: 分配内存给 Singleton 对象。

Step 2: 在分配的内存上调用构造函数,初始化 Singleton 对象。

Step 3: 将内存地址赋值给 instance 指针。

编译器或 CPU 为了优化性能,可能会进行指令重排序,不能保证2 和 3的执行顺序

使用原子操作和内存序来确保正确的顺序

LazySingleton.h

#include "mutex"
///懒汉式单例
class LazySingleton {
public:
    static LazySingleton *getInstance();
    LazySingleton(const LazySingleton &) = delete;
    LazySingleton &operator=(const LazySingleton &) = delete;
private:
    LazySingleton() = default;
    ~LazySingleton() = default;
    static std::atomic<LazySingleton*> m_instance;
    static std::mutex m_mutex; //锁也必须是静态的,这样才能在所有对象实例间共享同一个锁
};

LazySingleton.cpp

//延迟构造--用的时候再进行构造
std::atomic<LazySingleton*> LazySingleton::m_instance{nullptr};
std::mutex LazySingleton::m_mutex;

LazySingleton* LazySingleton::getInstance()
{
    auto tmp = m_instance.load(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 LazySingleton();
            /// 确保构造完成之后再赋值调用
            m_instance.store(tmp, std::memory_order_release);
        }
    }
    return tmp;
}

实现三:call_once

  • std::once_flag 和 std::call_once 提供了一种线程安全的一次性初始化机制,适用于单例模式等场景。它们比手动实现双重检查锁定更安全、更简洁。

CallOnceLazySingleton.h

#pragma once
#include <mutex>
class CallOnceLazySingleton {
public:
    static CallOnceLazySingleton *getInstance();

    CallOnceLazySingleton(const CallOnceLazySingleton &) = delete;
    CallOnceLazySingleton &operator=(const CallOnceLazySingleton &) = delete;
private:
    static void createInstance();
    CallOnceLazySingleton() = default;
    ~CallOnceLazySingleton() = default;
    static CallOnceLazySingleton*  m_instance;
    static std::once_flag onceFlag;
};

CallOnceLazySingleton.cpp

CallOnceLazySingleton*  CallOnceLazySingleton::m_instance{nullptr};
std::once_flag CallOnceLazySingleton::onceFlag;

CallOnceLazySingleton* CallOnceLazySingleton::getInstance()
{
    std::call_once(onceFlag,createInstance);
    return m_instance;
}

void CallOnceLazySingleton::createInstance()
{
    m_instance = new CallOnceLazySingleton();
};

实现四:静态局部变量

C++11之后规定了局部静态变量的初始化是线程安全的,编译器会自动的生成等效于双重检查锁定的线程安全代码

  • Scott Meyer在《Effective C++》中提出了一种简洁的singleton的写法
class Singleton{
    static Singleton& getInstance(){
        static Singleton instance;
        return instance;
    }
    private:
    Singleton()=default;
}

实现五:优雅简洁的工程实现

  • 使用了模板,可以轻松为任何类型创建单例

Singleton.h

#pragma once
template <typename T>
class  Singleton {
public:
    static T& Instance();
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

private:
    Singleton() = default;
    ~Singleton() = default;
};

template <typename T>
T& Singleton<T>::Instance() {
    static T instance;
    return instance;
}