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

在多线程环境下,单例模式(Singleton)是一种常见的设计模式,它保证一个类只有一个实例,并为全局提供访问点。实现线程安全的单例模式,既要保证对象只被实例化一次,又要避免因竞争条件导致多线程间的访问冲突。下面从经典实现、C++11后的改进以及性能优化三方面展开讨论。

1. 经典实现:双重检查锁(Double‑Check Locking)

class Singleton {
public:
    static Singleton& instance() {
        if (!ptr) {                     // 第一次检查(无锁)
            std::lock_guard<std::mutex> lock(mtx); // 进入临界区
            if (!ptr) {                 // 第二次检查(有锁)
                ptr = new Singleton();
            }
        }
        return *ptr;
    }
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

private:
    Singleton() = default;
    static Singleton* ptr;
    static std::mutex mtx;
};

Singleton* Singleton::ptr = nullptr;
std::mutex Singleton::mtx;

缺点

  1. 性能损耗:每次访问都需要进行一次锁操作,即使实例已创建也会有一次无谓的锁检查。
  2. 可移植性问题:在旧版编译器下,new 的返回值可能不是完全初始化的内存,导致数据竞争。
  3. 缺少销毁机制:单例对象在程序结束前不会被自动销毁,可能导致资源泄露。

2. C++11 之后的推荐做法:函数内部静态变量

自 C++11 起,局部静态变量的初始化是线程安全的。只需写一个 getInstance 函数,返回局部静态对象的引用即可。

class Singleton {
public:
    static Singleton& instance() {
        static Singleton instance; // 线程安全初始化
        return instance;
    }
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

private:
    Singleton() = default;
};

优点

  • 简洁易读:只需一行代码实现单例。
  • 天然线程安全:编译器保证在第一次访问时只初始化一次。
  • 资源自动释放:程序退出时静态对象会被销毁,释放资源。

注意事项

  • 若单例需要在析构时做特殊操作(例如释放外部资源),可在类中自定义析构函数。
  • 在多进程环境下,每个进程拥有独立的单例实例,不能共享。

3. 延迟销毁(Lazy Destruction)

在某些应用场景下,需要在程序终止前显式销毁单例,避免析构顺序导致的资源访问冲突。可以使用 std::unique_ptr 与自定义销毁函数:

class Singleton {
public:
    static Singleton& instance() {
        std::call_once(flag, [](){
            ptr.reset(new Singleton);
            std::atexit([](){ ptr.reset(); });
        });
        return *ptr;
    }
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

private:
    Singleton() = default;
    static std::unique_ptr <Singleton> ptr;
    static std::once_flag flag;
};

std::unique_ptr <Singleton> Singleton::ptr = nullptr;
std::once_flag Singleton::flag;

std::call_once 保障只调用一次,std::atexit 在程序退出前销毁单例。

4. 性能优化

如果单例对象只读且使用频繁,建议使用 std::shared_ptrstd::atomic 加速访问:

class Singleton {
public:
    static std::shared_ptr <Singleton> instance() {
        std::shared_ptr <Singleton> tmp = ptr.load(std::memory_order_acquire);
        if (!tmp) {
            std::lock_guard<std::mutex> lock(mtx);
            tmp = ptr.load(std::memory_order_relaxed);
            if (!tmp) {
                tmp = std::make_shared <Singleton>();
                ptr.store(tmp, std::memory_order_release);
            }
        }
        return tmp;
    }

private:
    Singleton() = default;
    static std::atomic<std::shared_ptr<Singleton>> ptr;
    static std::mutex mtx;
};
  • std::atomic 提供了无锁读取,减少竞争。
  • 由于 std::shared_ptr 采用引用计数,内存管理更灵活。

5. 总结

  • C++11 及以后:首选局部静态变量实现,简洁且线程安全。
  • 需要控制销毁顺序:结合 std::call_once + std::atexit
  • 高频访问:可使用 std::atomic<std::shared_ptr> 优化读取性能。

通过上述方法,既能确保单例的唯一性,又能兼顾多线程安全与性能,是现代 C++ 开发中的最佳实践。

发表评论