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

在多线程环境下,单例模式是常见的设计模式之一,旨在保证某个类只有一个实例并提供全局访问点。实现线程安全的单例有多种方法,下面将分别介绍几种常见的实现方式,并讨论它们的优缺点。

1. 经典的双重检查锁(Double-Check Locking)

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

    // 其他成员函数

private:
    Singleton() = default;
    ~Singleton() = default;
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

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

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

优点

  • 只在第一次访问时加锁,后续访问性能接近无锁。

缺点

  • 在C++11之前的编译器,instance_ 的写入可能被编译器重排,导致读线程看到部分初始化的对象。
  • 需要手动管理内存,可能会导致程序退出时资源未释放。

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

class Singleton {
public:
    static Singleton& getInstance() {
        static Singleton instance;   // 第一次进入时初始化
        return instance;
    }

private:
    Singleton() = default;
    ~Singleton() = default;
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
};

优点

  • 线程安全保证由 C++11 标准保证(函数内部的静态局部对象初始化是线程安全的)。
  • 简单易读,无需显式锁。
  • 对象在程序结束时自动析构,避免内存泄漏。

缺点

  • 如果单例在程序启动前被使用,可能会产生死锁(极少见)。
  • 对于需要延迟初始化时的某些特殊场景,可能不够灵活。

3. std::call_oncestd::once_flag

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

private:
    Singleton() = default;
    ~Singleton() = default;
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

    static Singleton* instance_;
    static std::once_flag initFlag_;
};

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

优点

  • call_once 配合使用,保证只初始化一次,且线程安全。
  • 与双重检查锁相比,避免了锁的手动管理。

缺点

  • 需要手动释放 instance_,或者在程序结束前进行销毁。
  • 代码略显冗长。

4. 通过 std::shared_ptrstd::unique_ptr

class Singleton {
public:
    static Singleton& getInstance() {
        static std::shared_ptr <Singleton> instance(new Singleton());
        return *instance;
    }

private:
    Singleton() = default;
    ~Singleton() = default;
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
};

优点

  • 利用智能指针自动管理资源。
  • 适合需要对单例对象进行共享引用计数的场景。

缺点

  • 引入引用计数,稍微增加开销。
  • 仍然依赖静态局部变量的初始化机制。

5. C++20 的 std::atomic<std::shared_ptr<>>std::make_shared

如果你在 C++20 环境下,需要在多个线程间安全地共享单例对象,可以考虑:

class Singleton {
public:
    static std::shared_ptr <Singleton> getInstance() {
        auto temp = instance_.load(std::memory_order_acquire);
        if (!temp) {
            std::lock_guard<std::mutex> lock(mutex_);
            temp = instance_.load(std::memory_order_relaxed);
            if (!temp) {
                temp = std::make_shared <Singleton>();
                instance_.store(temp, std::memory_order_release);
            }
        }
        return temp;
    }

private:
    Singleton() = default;
    ~Singleton() = default;
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

    static std::atomic<std::shared_ptr<Singleton>> instance_;
    static std::mutex mutex_;
};

std::atomic<std::shared_ptr<Singleton>> Singleton::instance_{nullptr};
std::mutex Singleton::mutex_;

优点

  • 线程安全且可通过 std::shared_ptr 方便管理。
  • 适用于需要在多线程中传递单例引用的场景。

缺点

  • 代码复杂度更高。
  • 依赖 C++20 特性,可能不兼容旧编译器。

小结

  • 最推荐:如果你使用的是 C++11 或更高版本,最简单、最安全的做法是 Meyers’ Singleton(静态局部变量)。
  • 需要显式销毁:如果你需要手动控制单例销毁时机,或在特殊多线程场景下使用,考虑 std::call_once 或双重检查锁。
  • 共享计数:当你希望在多个线程或模块共享同一个单例实例时,使用 std::shared_ptrstd::atomic<std::shared_ptr> 更合适。

记住,单例模式的核心是“全局唯一”,但过度使用单例可能导致代码耦合度高、难以测试和维护。建议在需求明确、不可避免的场景下才使用单例,或者考虑更现代的设计方案(如依赖注入、服务定位器等)。

发表评论