在现代 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_ptr 与 std::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::atexit 或 std::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_once 与 std::shared_ptr 的组合。它们提供了极简的语法、保证线程安全,并且对性能影响最小。无论你选择哪种实现,都建议:
- 删除拷贝构造与赋值操作,确保实例唯一。
- 使用
static成员或局部静态来实现延迟初始化。 - 在多线程场景下进行彻底的并发测试,以排除竞争问题。
通过上述技巧,你可以在任何 C++ 项目中安全、简洁地实现单例模式。