单例模式

Posted by     "LETTER" on Wednesday, April 30, 2025

单例模式是一种创建型设计模式,能够保证一个类只有唯一的实例存在, 并提供一个访问该实例的全局节点,基于该特性,单例模式广泛应用于线程池管理、日志管理和全局配置参数管理等场景。

无法在多进程中使用

单例模式有以下三个设计要点:

  1. 提供对唯一实例的全局访问点

    实例的唯一性通过在类内部将实例声明为 private static 来实现,实例访问点的全局性通过将对应方法(通常命名为 GetInstanceInstance)声明为 static 来实现。

  2. 多线程安全

    单例模式的实现通常分为饿汉式单例和懒汉式单例。饿汉式单例天然地具有多线程安全性,懒汉式单例的单线程版本容易实现,多线程版本需要做些额外措施以确保多线程场景下的安全性。

  3. 防止私自创建实例

    因为对单例实例的创建及获取只应通过上述的全局访问点实现,所以应防止用户私自创建实例,通常通过私有化默认构造(default),并禁止拷贝构造(copy)和拷贝赋值(assign)来实现。

饿汉式单例

饿汉式单例在类的加载过程中便迫不及待地(“饿”的体现)为实例指针分配了动态内存,当有多个线程尝试获取实例指针时,获得的将是同一个实体,因此饿汉式单例天然地具有多线程安全性,但也正因为实例的动态内存分配过程未考虑访问时机,因而会降低程序启动速度。下面是典型的饿汉式单例实现:

class HungrySingleton
{
private:
    static HungrySingleton *pinstance_;

private:
    HungrySingleton() {}
    HungrySingleton(const HungrySingleton &) = delete;
    HungrySingleton &operator=(const HungrySingleton &) = delete;

public:
    ~HungrySingleton() {}

public:
    static HungrySingleton *GetInstance();
};

HungrySingleton *HungrySingleton::pinstance_ = new HungrySingleton;

HungrySingleton *HungrySingleton::GetInstance()
{
    return pinstance_;
}

懒汉式单例

不同于饿汉式单例,懒汉式单例仅在初次执行获取实例的动作时才对实例进行动态内存分配(“懒”的体现),因而程序具有更高的启动速度,但无可避免地会降低初次访问实例时的效率。同时,使用懒汉式单例时需要注意多线程场景的下的安全问题。

适用于单线程场景的懒汉式单例

单线程场景下的懒汉式单例实现较为简单,将实例动态内存分配过程放到实例访问点 GetInstance 中,通过检查实例指针是否为空来判断是否是初次访问,且仅在初次访问时为实例分配内存即可:

class LazySingleton
{
private:
    static LazySingleton *pinstance_;

private:
    LazySingleton() {}
    LazySingleton(const LazySingleton &) = delete;
    LazySingleton &operator=(const LazySingleton &) = delete;

public:
    ~LazySingleton() {}

public:
    static LazySingleton *GetInstance();
};

LazySingleton *LazySingleton::pinstance_{nullptr};

LazySingleton *LazySingleton::GetInstance()
{
    if (nullptr == pinstance_)
    {
        pinstance_ = new LazySingleton;
    }
    return pinstance_;
}

假设实例指针尚为空时,有多个线程同时调用 GetInstance 方法,则会造成每个线程获取到各自的实例指针,违反了单例模式中的实例唯一性原则。

多线程安全的懒汉式单例:单检锁实现

可以通过在 GetInstance 方法中添加互斥锁 mutex 来解决多线程场景下的资源争抢问题,同时,为了实现自解锁,通常使用 mutex 的 RAII(Resource Acquisition Is Initialization)包装器类 std::lock_guardstd::lock_guard 是 C++11 定义于 <mutex> 中的新特性,std::lock_guard 对象可以在生命周期结束前通过析构函数自动对其所管理的 mutex 对象执行 unlock 操作。下面是典型的直接基于互斥锁的懒汉式单例实现:

class LazySingleton
{
private:
    static LazySingleton *pinstance_;
    static std::mutex mutex_;

private:
    LazySingleton() {}
    LazySingleton(const LazySingleton &) = delete;
    LazySingleton &operator=(const LazySingleton &) = delete;

public:
    ~LazySingleton() {}

public:
    static LazySingleton *GetInstance();
};

LazySingleton *LazySingleton::pinstance_{nullptr};
std::mutex LazySingleton::mutex_;

LazySingleton *LazySingleton::GetInstance()
{
    std::lock_guard<std::mutex> lock(mutex_);
    if (nullptr == pinstance_)
    {
        pinstance_ = new LazySingleton;
    }
    return pinstance_;
}

上面的实现方式我们通常称之为单检锁模式,即每次调用 GetInstance 方法尝试获取实例时都会执行加锁操作,并在自析构 std::lock_guard 对象时执行解锁操作,这必然会降低实例访问效率,因为如果已经为实例指针分配了内存得话,显然调用 GetInstance 时直接将实例指针返回即可,意即,只有初次调用 GetInstance 时才有必要执行锁操作。

多线程安全的懒汉式单例:双检锁 + 原子变量实现

使用双检锁确保性能

针对单检锁方法中存在的性能问题,有一种所谓的双检锁模式(Double-Checked Locking Pattern,DCLP)优化方案,即在 GetInstance 中执行锁操作前,在最外层额外地进行一次实例指针的检查操作(“双检”的体现),这样可以保证实例指针完成内存分配后,单纯的实例访问操作不会再附带锁操作带来的性能开销:

class LazySingleton
{
private:
    static LazySingleton *pinstance_;
    static std::mutex mutex_;

private:
    LazySingleton() {}
    LazySingleton(const LazySingleton &) = delete;
    LazySingleton &operator=(const LazySingleton &) = delete;

public:
    ~LazySingleton() {}

public:
    static LazySingleton *GetInstance();
};

LazySingleton *LazySingleton::pinstance_{nullptr};
std::mutex LazySingleton::mutex_;

LazySingleton *LazySingleton::GetInstance()
{
    if (nullptr == pinstance_)
    {
        std::lock_guard<std::mutex> lock(mutex_);
        if (nullptr == pinstance_)
        {
            pinstance_ = new LazySingleton;
        }
    }
    return pinstance_;
}

双检锁方法初衷虽好,但却破坏了多线程场景下的安全性,这是由动态内存分配时 new 底层操作的非原子性导致的,执行 pinstance_ = new LazySingleton; 语句时,底层其实对应了三个步骤:

  1. 向系统申请分配内存,大小为 sizeof(LazySingleton)
  2. 调用 LazySingleton 的默认构造函数在申请的内存上构造出实例
  3. 返回申请内存的指针给 pinstance_

根本问题在于,上面的这三个步骤无法确保执行顺序。例如,出于优化的原因,处理器很可能调整步骤 3 和步骤 2 的执行顺序(按照 1、3、2 的顺序执行)。

假设,现在某个线程执行到了 pinstance_ = new LazySingleton; 语句,底层操作完成了内存申请(步骤 1)和实例指针赋值(步骤 3),但尚未完成申请内存的构造(步骤 2),意即,现在 pinstance_ 指向的是一片脏内存。此时,另一个线程恰好执行到双检锁的最外层检查,该线程发现 pinstance_ 非空(发生了脏读),检查为 false,因而直接取走了尚未完成构造的实例指针(return pinstance_;),从而可能诱发程序未定义行为(undefined behavior)。

使用原子变量确保多线程安全性

可以通过封装一个单例指针类型的 std::atomic 原子对象,将单例指针的读写操作转化为对原子对象的操作,以此来确保双检锁实现的懒汉式单例的多线程安全性。

std::atomic 是 C++11 定义于 <atomic> 中的新特性,每个 std::atomic 模板的实例化和全特化定义一个原子类型,若一个线程写入原子对象,同时另一线程从它读取,则行为良好定义。另外,对原子对象的访问可以建立线程间同步,并按 std::memory_order 枚举类型中的枚举常量对非原子内存访问定序:

typedef enum memory_order {
    memory_order_relaxed,
    memory_order_consume,
    memory_order_acquire,
    memory_order_release,
    memory_order_acq_rel,
    memory_order_seq_cst
} memory_order;

std::memory_order 涉及到内存模型(memory model)、内存序(memory order)、内存栅栏(memory fence)等诸多复杂概念,此处不予展开。下面给出经典的基于双检锁 + 原子变量的懒汉式单例实现:

class LazySingleton
{
private:
    static std::atomic<LazySingleton *> ainstance_;
    static std::mutex mutex_;

private:
    LazySingleton() {}
    LazySingleton(const LazySingleton &) = delete;
    LazySingleton &operator=(const LazySingleton &) = delete;

public:
    ~LazySingleton() {}

public:
    static LazySingleton *GetInstance();
};

std::atomic<LazySingleton *> LazySingleton::ainstance_;
std::mutex LazySingleton::mutex_;

LazySingleton *LazySingleton::GetInstance()
{
    LazySingleton *tmp = ainstance_.load(std::memory_order_acquire);
    if (nullptr == tmp)
    {
        std::lock_guard<std::mutex> lock(mutex_);
        tmp = ainstance_.load(std::memory_order_relaxed);
        if (nullptr == tmp)
        {
            tmp = new LazySingleton;
            ainstance_.store(tmp, std::memory_order_release);
        }
    }
    return tmp;
}

作两点说明:

  1. load 方法:原子性地加载并返回原子变量的当前值,类似读操作。唯一形参类型为 std::memory_order,默认值为 memory_order_seq_cst
  2. store 方法:根据第一实参原子性地替换原子变量的当前值,类似写操作。第二形参类型为 std::memory_order,默认值为 memory_order_seq_cst

上面这种原子变量的使用方式称为 Acquire-Release Semantic 内存模型,如果保持 loadstorestd::memory_order 参数缺省,则成为 Sequential Consistency 内存模型,性能会稍有损失。

百度 Apollo 项目中有用到双检锁 + 原子变量的懒汉式单例实现,例如 Cyber RT 框架中的调度器设计,具体实现在 apollo\cyber\scheduler\scheduler_factory.cc 中。

Meyers单例模式

Meyers单例是 Scott Meyers在《Effective C++》中提出的编程范式,其实现非常优雅,一般采用该模式:

class MeyersSingleton
{
private:
    MeyersSingleton() = default;       // 禁止外部直接构造
    ~MeyersSingleton() = default; 	   // 禁止外部直接析构

    MeyersSingleton(const MeyersSingleton &) = delete;				// 禁止拷贝构造
    MeyersSingleton &operator=(const MeyersSingleton &) = delete;	// 禁止拷贝赋值
public:
    static MeyersSingleton &GetInstance(){
        static MeyersSingleton instance;
        return instance;
    }
};

Meyers单例本质上也是一种懒汉式实现,但其在 C++11及以后的标准中是天然多线程安全的,因为自 C++11起规定

If multiple threads attempt to initialize the same static local variable concurrently, the initialization occurs exactly once (similar behavior can be obtained for arbitrary functions with std::call_once). Note: usual implementations of this feature use variants of the double-checked locking pattern, which reduces runtime overhead for already-initialized local statics to a single non-atomic boolean comparison.

翻译过来就是,如果多个线程同时尝试初始化相同的静态局部变量,初始化动作只会发生一次,这个内部特性通常也是通过双检锁模式实现的。

apollo单例实现

apollo中通过宏定义实现单例,可通过DECLARE_SINGLETON(class)将任意一个类修饰为单例类

#ifndef SINGLETON_MACROS_H_
#define SINGLETON_MACROS_H_

#include <memory>
#include <mutex>
#include <type_traits>

// 类型萃取模板类
// 检查模板类型参数T中是否包含与宏参数func同名的方法
// 包含,则模板类的 value 成员被置为 true,
// 不包含,置为 false。
// 注意:func 在 T 中必须是公有的,否则无法被发现
#define DEFINE_TYPE_TRAIT(name, func)                     \
  template <typename T>                                   \
  struct name {                                           \
    template <typename Class>                             \
    static constexpr bool Test(decltype(&Class::func)*) { \
      return true;                                        \
    }                                                     \
    template <typename>                                   \
    static constexpr bool Test(...) {                     \
      return false;                                       \
    }                                                     \
                                                          \
    static constexpr bool value = Test<T>(nullptr);       \
  };                                                      \
                                                          \
  template <typename T>                                   \
  constexpr bool name<T>::value;

// 判断是否存在Shutdown函数
DEFINE_TYPE_TRAIT(HasShutdown, Shutdown)

template <typename T>
typename std::enable_if<HasShutdown<T>::value>::type CallShutdown(T *instance) {
  instance->Shutdown();
}

template <typename T>
typename std::enable_if<!HasShutdown<T>::value>::type CallShutdown(
    T *instance) {
  (void)instance;
}

// There must be many copy-paste versions of these macros which are same
// things, undefine them to avoid conflict.
// 单例模式宏
#undef UNUSED
#undef DISALLOW_COPY_AND_ASSIGN

#define UNUSED(param) (void)param

#define DISALLOW_COPY_AND_ASSIGN(classname) \
  classname(const classname &) = delete;    \
  classname &operator=(const classname &) = delete;

#define DECLARE_SINGLETON(classname)                                      \
 public:                                                                  \
  static classname *Instance(bool create_if_needed = true) {              \
    static classname *instance = nullptr;                                 \
    if (!instance && create_if_needed) {                                  \
      static std::once_flag flag;                                         \
      std::call_once(flag,                                                \
                     [&] { instance = new (std::nothrow) classname(); }); \
    }                                                                     \
    return instance;                                                      \
  }                                                                       \
                                                                         \
  static void CleanUp() {                                                 \
    auto instance = Instance(false);                                      \
    if (instance != nullptr) {                                            \
      CallShutdown(instance);                                             \
    }                                                                     \
  }                                                                       \
                                                                          \
 private:                                                                 \
  classname();                                                            \
  DISALLOW_COPY_AND_ASSIGN(classname)

#endif  // SINGLETON_MACROS_H_