在多线程环境下,单例模式是一个常见的需求,它保证了全局只有一个实例,并且能够被多个线程安全地访问。下面从设计思路、实现方式、以及性能优化几个方面来详细阐述。
1. 设计思路
单例的核心要点是:
- 私有化构造函数,防止外部实例化。
- 提供全局访问入口,通常是一个静态成员函数。
- 延迟初始化,即在第一次使用时才创建实例。
- 保证线程安全,避免多线程同时创建多个实例。
在C++11之后,编译器对static局部变量的初始化做了线程安全保证,简化了实现。
2. 基本实现(C++11+)
class Singleton {
public:
// 提供全局访问入口
static Singleton& getInstance() {
// 局部静态变量的初始化是线程安全的
static Singleton instance;
return instance;
}
// 删除拷贝构造和赋值运算符,防止复制
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
// 示例业务方法
void doSomething() {
std::cout << "Singleton doing something. Thread ID: " << std::this_thread::get_id() << std::endl;
}
private:
// 私有构造函数
Singleton() { std::cout << "Singleton constructed." << std::endl; }
~Singleton() = default;
};
说明
getInstance()通过局部静态变量实现延迟初始化,且在多线程环境下只会被初始化一次。- 删除拷贝构造和赋值运算符,防止外部复制实例。
~Singleton()声明为默认析构,若需要自定义析构,可自行实现。
3. 延迟销毁
上面的实现会在程序退出时销毁单例实例,但有时我们希望单例永不销毁,避免销毁顺序导致的资源访问错误。可以采用“静态局部对象+永不销毁”策略:
static Singleton& getInstance() {
static Singleton* instance = new Singleton;
return *instance;
}
此时需要手动在程序结束前 delete 单例,或依赖系统释放。
4. 经典 Meyers 单例
Meyers 单例即上面所示的基本实现。由于C++11保证了线程安全,这种实现已成为主流。
5. 对比:双重检查锁(Double-Check Locking)
在C++11之前,需要手动实现线程同步,常见的是双重检查锁:
class Singleton {
public:
static Singleton* getInstance() {
if (!instance) {
std::lock_guard<std::mutex> lock(mtx);
if (!instance) {
instance = new Singleton();
}
}
return instance;
}
private:
static Singleton* instance;
static std::mutex mtx;
};
Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mtx;
但该实现容易出现指令重排导致的线程安全问题,C++11以后不推荐使用。
6. 性能优化
- 懒加载:如上述实现,实例只在首次调用时创建,节省资源。
- 无锁实现:Meyers 单例利用编译器的线程安全局部静态,几乎无锁。
- 内存占用:使用单例时请注意全局对象的生命周期,避免因单例持有大量资源导致程序长时间占用。
7. 单例的局限性
- 可测试性差:全局状态会让单元测试变得困难。
- 隐藏依赖:使用单例往往让类之间的依赖关系隐式存在。
- 多实例需求:如果需求变化,单例会成为单点故障。
8. 结语
在C++11以后,实现线程安全单例变得简单且可靠。只需使用局部静态变量即可满足大多数场景。若业务需要更复杂的初始化或销毁策略,可在此基础上进行扩展。记住:单例应仅在确有必要时使用,保持代码的可维护性与可测试性。