**标题:C++中实现线程安全的单例模式——Meyers单例与双重检查锁**

在C++中,单例模式常用于需要全局唯一实例的场景,例如日志系统、配置管理器或数据库连接池。实现单例时的主要难点在于如何保证线程安全,同时避免不必要的性能开销。下面我们分别介绍两种常见实现:Meyers单例(C++11之后的线程安全局部静态)和双重检查锁(Double-Check Locking,DCL)结合C++11原子操作的方案。


1. Meyers单例(C++11之后的线程安全局部静态)

class Logger {
public:
    static Logger& instance() {
        static Logger logger;   // C++11保证线程安全的初始化
        return logger;
    }

    void log(const std::string& msg) {
        std::lock_guard<std::mutex> lock(mutex_);
        std::cout << msg << std::endl;
    }

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

    std::mutex mutex_;
};

优点

  • 简洁:只需一行代码即可完成实例化。
  • 线程安全:自C++11起,局部静态变量的初始化是线程安全的。
  • 延迟初始化:只有第一次调用instance()时才会构造对象。

缺点

  • 销毁顺序:若在多线程环境中程序终止,可能会出现“静态析构顺序问题”。可通过显式销毁函数或std::atexit解决。

2. 双重检查锁(Double-Check Locking)与原子操作

早期的C++实现中常用双重检查锁来延迟初始化并减少锁开销。现代C++可以结合std::atomicstd::call_once进一步简化。

传统双重检查锁(不推荐)

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

private:
    Config() = default;
    static Config* ptr_;
    static std::mutex mutex_;
};

Config* Config::ptr_ = nullptr;
std::mutex Config::mutex_;

问题:在某些编译器/硬件上,内存重排可能导致ptr_在构造完成前被写入,导致其他线程获取到不完整的对象。

使用std::call_once(推荐)

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

private:
    Config() = default;
    static Config* ptr_;
    static std::once_flag flag_;
};

Config* Config::ptr_ = nullptr;
std::once_flag Config::flag_;
  • std::call_once确保只执行一次初始化,并且对所有线程都是可见的。
  • 省略手动锁,代码更简洁且安全。

3. 现代C++实现:std::shared_ptrstd::make_shared

如果单例对象需要动态释放或需要共享所有权,可使用std::shared_ptr

class Service {
public:
    static std::shared_ptr <Service> instance() {
        std::call_once(flag_, [](){
            ptr_ = std::make_shared <Service>();
        });
        return ptr_;
    }

private:
    Service() = default;
    static std::shared_ptr <Service> ptr_;
    static std::once_flag flag_;
};

std::shared_ptr <Service> Service::ptr_ = nullptr;
std::once_flag Service::flag_;
  • 通过std::make_shared一次性分配对象和控制块,减少内存碎片。
  • std::shared_ptr在程序结束时会自动析构,避免手动管理。

4. 小结

方案 线程安全 代码量 典型使用场景
Meyers单例 1行 只需一次构造,无需手动销毁
双重检查锁 ⚠️ 旧版 约30行 传统实现,易出错
std::call_once 约10行 推荐现代C++实现
std::shared_ptr + std::call_once 约10行 需要共享所有权或动态释放
  • 对于C++11及以后,最推荐的做法是使用std::call_once(或Meyers单例),它既安全又简洁。
  • 若对单例生命周期有特殊需求(例如在多进程间共享),则需考虑更复杂的方案(如映射文件或信号量)。

提示:在高并发场景下,避免频繁锁定单例内部对象。可使用细粒度锁或无锁算法来提升性能。


发表评论