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

在现代 C++ 开发中,单例模式经常被用于需要全局唯一实例的场景,例如日志系统、配置管理器或数据库连接池。实现线程安全的单例模式时,需要确保在多线程环境下只有一次实例化,且不产生竞争条件或性能瓶颈。以下几种实现方式可以满足这些需求,并兼顾可读性和可维护性。

1. 局部静态变量(C++11 之后的线程安全初始化)

class Singleton {
public:
    static Singleton& instance() {
        static Singleton instance; // C++11 之后保证线程安全
        return instance;
    }

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

    // 示例方法
    void doSomething() { /* ... */ }

private:
    Singleton() {}   // 私有构造函数
};

优点

  • 简洁:代码只有几行,易于维护。
  • 线程安全:C++11 及以后标准保证对 static 局部变量的初始化是线程安全的。
  • 延迟初始化:实例在第一次使用时才创建,避免不必要的资源占用。

缺点

  • 初始化时机不确定:如果需要在程序入口时就初始化,需手动调用 instance()
  • 无法控制析构顺序:全局对象的析构顺序不确定,可能导致某些全局资源提前释放。

2. 带双重检查锁(双重检验锁)+ std::call_once

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

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

private:
    Singleton() {}
    static std::unique_ptr <Singleton> instance_;
    static std::once_flag initFlag_;
};

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

优点

  • 明确的初始化顺序:可以在 instance() 之外的地方调用,保证在任何地方都能安全使用。
  • 只进行一次初始化std::call_once 只会执行一次,避免多次竞争。

缺点

  • 略显冗长:相比局部静态变量多几行代码。
  • 微小的性能开销:每次调用 instance() 都会检查 std::once_flag,但这个开销已被优化到极低。

3. 传统互斥锁 + 懒汉式(线程安全)

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

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

private:
    Singleton() {}
    static std::unique_ptr <Singleton> instance_;
    static std::mutex mtx_;
};

std::unique_ptr <Singleton> Singleton::instance_;
std::mutex Singleton::mtx_;

优点

  • 兼容旧标准:适用于 C++11 之前的编译器。
  • 易于理解:使用 mutex 进行同步,直观。

缺点

  • 性能影响:每次调用 instance() 都会锁定 mutex,导致高并发下性能下降。
  • 更易产生死锁:若在构造函数中再次访问 instance(),可能导致自旋死锁。

4. Meyers 单例(静态局部变量 + std::shared_ptr

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

    // ...
private:
    Singleton() {}
};

优点

  • 自动析构shared_ptr 的析构函数会在程序退出时调用,避免手动管理。
  • 灵活共享:可以将 instance() 返回值复制给其他对象,方便共享。

缺点

  • 额外的引用计数开销:虽然一般 negligible,但在极高并发下可能产生不必要的开销。

5. 原子指针 + 延迟初始化(无锁实现)

class Singleton {
public:
    static Singleton& instance() {
        Singleton* tmp = instance_.load(std::memory_order_acquire);
        if (!tmp) {
            std::lock_guard<std::mutex> lock(mtx_);
            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() {}
    static std::atomic<Singleton*> instance_;
    static std::mutex mtx_;
};

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

优点

  • 无锁访问:在已经实例化后,访问不需要锁。
  • 细粒度控制:可根据需要选择内存顺序。

缺点

  • 实现复杂:需要仔细处理内存顺序和双重检查。
  • 潜在的 ABA 问题:若单例被销毁后再次创建,需额外注意。

6. 结合 std::shared_ptrstd::call_once(最佳实践)

class Singleton {
public:
    static Singleton& instance() {
        std::call_once(initFlag_, []() {
            instance_ = std::make_shared <Singleton>();
        });
        return *instance_;
    }

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

private:
    Singleton() {}
    static std::shared_ptr <Singleton> instance_;
    static std::once_flag initFlag_;
};

std::shared_ptr <Singleton> Singleton::instance_;
std::once_flag Singleton::initFlag_;

适用场景

  • 需要在多线程中安全初始化,且想要 shared_ptr 自动管理生命周期。
  • 兼顾性能与可读性,适合大多数项目。

7. 线程安全的延迟销毁

有时单例在程序退出前会被销毁,导致访问已销毁对象。为避免此问题,可以使用 std::atexitstd::shared_ptr 的自定义删除器。示例:

class Singleton {
public:
    static Singleton& instance() {
        std::call_once(initFlag_, []() {
            instance_ = std::shared_ptr <Singleton>(new Singleton(), 
                [](Singleton* p){ delete p; std::cout << "Singleton destroyed\n"; });
        });
        return *instance_;
    }

private:
    Singleton() {}
    static std::shared_ptr <Singleton> instance_;
    static std::once_flag initFlag_;
};

8. 单例的单元测试注意事项

  • 避免全局状态污染:在测试前后手动重置单例。
  • 使用 std::unique_ptr 重置:在测试框架中调用 Singleton::reset()(自定义方法)来重新初始化。
  • 多线程测试:使用 std::thread 并行调用 instance(),验证同一实例被共享。

结语

在现代 C++ 中,最推荐的实现方式是使用 C++11 之后的局部静态变量std::call_oncestd::shared_ptr 的组合。它们提供了极简的语法、保证线程安全,并且对性能影响最小。无论你选择哪种实现,都建议:

  1. 删除拷贝构造与赋值操作,确保实例唯一。
  2. 使用 static 成员或局部静态来实现延迟初始化。
  3. 在多线程场景下进行彻底的并发测试,以排除竞争问题。

通过上述技巧,你可以在任何 C++ 项目中安全、简洁地实现单例模式。

发表评论