如何在C++中实现线程安全的单例模式?

在多线程环境下,单例模式需要保证以下两点:

  1. 只创建一次实例;
  2. 多线程并发访问时不产生竞态条件。

下面介绍几种常用实现方式,并讨论它们的优缺点。

1. 经典懒汉式 + 双重检查锁(Double‑Check Locking)

class Singleton {
public:
    static Singleton& instance() {
        if (instance_ == nullptr) {               // 第一次检查
            std::lock_guard<std::mutex> lock(mutex_);
            if (instance_ == nullptr) {           // 第二次检查
                instance_ = new Singleton();
            }
        }
        return *instance_;
    }

    // 禁止拷贝构造和赋值
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

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

    static Singleton* instance_;
    static std::mutex mutex_;
};

Singleton* Singleton::instance_ = nullptr;
std::mutex Singleton::mutex_;

优点

  • 延迟初始化,真正需要时才创建实例。
  • 线程安全,使用 std::mutex 防止竞态。

缺点

  • 代码较为繁琐。
  • 在 C++11 以前的编译器中,instance_ 的写操作可能不可见,导致缺陷;C++11 之后已修正。

2. Meyer’s Singleton(局部静态变量)

class Singleton {
public:
    static Singleton& instance() {
        static Singleton instance;   // 线程安全的局部静态变量
        return instance;
    }

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

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

C++11 以后,局部静态变量的初始化是线程安全的,编译器会在第一次进入 instance() 时保证单例被正确构造。

优点

  • 代码最简洁。
  • 自动销毁,程序退出时析构函数被调用。

缺点

  • 仍是懒汉式,如果实例创建时出现异常,后续调用会再次尝试。
  • 对析构顺序要求严格,若单例持有全局资源,可能导致析构顺序不确定。

3. 静态局部对象 + std::call_once

class Singleton {
public:
    static Singleton& instance() {
        std::call_once(flag_, [](){ instance_ = new Singleton(); });
        return *instance_;
    }

    ~Singleton() {
        delete instance_;
        instance_ = nullptr;
    }

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

private:
    Singleton() = default;
    static Singleton* instance_;
    static std::once_flag flag_;
};

Singleton* Singleton::instance_ = nullptr;
std::once_flag Singleton::flag_;

优点

  • Meyers 兼容,确保单例只被初始化一次。
  • std::once_flag 只需要一次检查,性能优于双重检查锁。

缺点

  • 需要手动管理析构,容易忘记。

4. 基于 std::shared_ptr 的单例(可自毁)

class Singleton {
public:
    static std::shared_ptr <Singleton> instance() {
        static std::shared_ptr <Singleton> instance(new Singleton(),
            [](Singleton* p){ delete p; });
        return instance;
    }

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

private:
    Singleton() = default;
};

使用 std::shared_ptr 可以让单例在不再使用时自动销毁,适用于需要在特定时机释放资源的场景。

优点

  • 自动销毁,避免全局静态析构顺序问题。

缺点

  • 需要额外的引用计数开销。

5. 线程安全的初始化顺序控制

如果单例中需要依赖其它全局对象,建议使用 “单例先构造,其他后析构” 的模式:

class Singleton {
public:
    static Singleton& instance() {
        static Singleton instance; // 首先构造
        return instance;
    }
    // ...
};

void foo() {
    // 使用单例
    Singleton::instance().doSomething();
}

因为 C++11 之后,编译器保证局部静态变量在第一次使用时初始化,并且在程序结束时按逆序析构,确保单例始终存在于其生命周期内。

小结

  • 最简洁:Meyer’s Singleton(局部静态对象)。
  • 性能最优std::call_once
  • 可自毁std::shared_ptr

在实际项目中,推荐使用 Meyer's Singletonstd::call_once 的组合,既保证线程安全,又代码简洁。若对资源释放有特殊需求,可考虑 std::shared_ptr 或手动析构方式。

发表评论