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

单例模式(Singleton)是一种常用的软件设计模式,确保一个类只有一个实例,并提供全局访问点。随着多线程编程的普及,单例实现必须保证线程安全。下面以 C++17 为例,介绍几种典型实现方式,并分析它们的优缺点。

1. 使用局部静态变量(Meyer’s Singleton)

class Singleton {
public:
    static Singleton& instance() {
        static Singleton instance; // C++11 起线程安全的局部静态初始化
        return instance;
    }
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

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

优点

  • 代码简洁,几乎不需要额外的同步机制。
  • C++11 之后,局部静态对象的初始化是线程安全的,编译器会自动生成必要的锁。
  • 资源在第一次使用时才创建,延迟初始化。

缺点

  • 只适用于 C++11 及以后版本。
  • 由于使用 static 对象,无法在程序结束时有序销毁(如果需要销毁顺序或自定义释放,则需要手动控制)。

2. 带双重检查锁(Double-Check Locking)

class Singleton {
public:
    static Singleton* instance() {
        Singleton* tmp = instance_.load(std::memory_order_acquire);
        if (!tmp) {
            std::lock_guard<std::mutex> lock(mutex_);
            tmp = instance_.load(std::memory_order_relaxed);
            if (!tmp) {
                tmp = new Singleton();
                instance_.store(tmp, std::memory_order_release);
            }
        }
        return tmp;
    }

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

private:
    Singleton() = default;
    ~Singleton() = default;
    static std::atomic<Singleton*> instance_;
    static std::mutex mutex_;
};

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

优点

  • 适用于多种编译器,兼容 C++98/03/11。
  • 延迟初始化,且线程安全。

缺点

  • 代码较为繁琐,容易出错。
  • 在某些极端情况下,可能因为编译器优化导致多次实例化(需要使用 std::atomic 及正确的内存序保证)。

3. 使用 std::call_once

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

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

private:
    Singleton() = default;
    ~Singleton() = default;
    static std::unique_ptr <Singleton> instance_;
    static std::once_flag init_flag_;
};

std::unique_ptr <Singleton> Singleton::instance_;
std::once_flag Singleton::init_flag_;

优点

  • 代码简洁,安全且跨编译器兼容。
  • std::call_once 保证初始化只执行一次,无需手动写锁。

缺点

  • 依赖 std::once_flag,C++11 之后可用。
  • 与第一种实现相同,实例在首次访问时创建,无法在程序结束时自定义销毁顺序。

4. 传统懒汉式(非线程安全)+ 后期加锁

class Singleton {
public:
    static Singleton& instance() {
        if (!instance_) {
            std::lock_guard<std::mutex> lock(mutex_);
            if (!instance_) instance_ = new Singleton();
        }
        return *instance_;
    }

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

该实现与双重检查锁类似,但缺乏 std::atomic 的使用,容易出现线程安全问题,推荐使用 std::call_once 或局部静态对象。

5. 采用 std::shared_ptrstd::weak_ptr

如果你需要在多线程中共享单例实例并允许其在无引用时自动销毁,可结合 std::shared_ptrstd::weak_ptr

class Singleton {
public:
    static std::shared_ptr <Singleton> getInstance() {
        std::shared_ptr <Singleton> tmp = instance_.lock();
        if (!tmp) {
            std::lock_guard<std::mutex> lock(mutex_);
            tmp = instance_.lock();
            if (!tmp) {
                tmp = std::shared_ptr <Singleton>(new Singleton);
                instance_ = tmp;
            }
        }
        return tmp;
    }

private:
    Singleton() = default;
    static std::weak_ptr <Singleton> instance_;
    static std::mutex mutex_;
};

优点

  • 单例可以在不被任何线程引用时自动销毁,避免内存泄漏。
  • 支持在多线程场景下安全获取实例。

缺点

  • 每次调用都要进行 std::weak_ptr::lock(),会产生一定开销。
  • 需要小心循环引用,避免泄漏。

小结

  • C++11 以上:推荐使用 局部静态变量std::call_once,两者都简单安全。
  • 兼容旧标准:可使用 双重检查锁std::call_once(如果支持 C++11)来实现线程安全。
  • 需要自动销毁:可以使用 std::shared_ptr + std::weak_ptr 的方案。

选择哪种实现,取决于项目的编译器支持、对实例生命周期的需求以及对代码可读性的要求。掌握这些实现方式后,你可以在多线程 C++ 项目中轻松、安全地使用单例模式。

发表评论