C++单例中也存在线程安全问题,这篇文章总结单例模式的各种创建方式,讨论利弊。单例的目的是为实现结构对象中只有一个实例,对于类的构造函数,拷贝构造函数、赋值构造函数应当做对应处理。普通构造函数和析构函数 应为private 级别,拷贝构造函数和赋值构造函数应当删除,防止构造多个对象。以下分析懒汉式、饿汉式、call_once 等构造方式及利弊分析。
1. 懒汉式
//MyClass.h
class MyClass{
public:
static MyClass* getInstance(){
if (m_instance == nullptr) {
m_instance = new MyClass();
}
return m_instance;
}
private:
MyClass() {} //私有构造函数
MyClass(const MyClass& my);//禁止拷贝构造函数
MyClass& operator=(const MyClass &my); //禁止赋值操作
static MyClass* m_instance;
}
//MyClass.cpp
MyClass* MyClass::m_instance = nullptr;// 存放在Cpp中,若放在.h中,.h被包含多次时重复定义
上面例子存在的问题是:
- 线程不安全
- 不能灵活析构单例
解决方案: 在getInstance()中使用m_instance时加锁
class MyClass{
private:
std::unique_lock<std::mutex> locker(m_mutex);
}
public:
static MyClass* getInstance(){
std::unique_lock<std::mutex> locker(m_mutex);
if (m_instance == nullptr) {
m_instance = new MyClass();
}
return m_instance;
}
static void destory(){
std::unique_lock<std::mutex> locker(m_mutex);
if (m_instalce) {
delete m_instance;
m_instance = nullptr;
}
}
以上代码解决了线程安全和单例内存释放的问题,但是如果单例对象被多次访问需要多次加锁,效率较低,可以使用双检查机制
static MyClass* getInstance(){
if (m_instance == nullptr) {
std::unique_lock<std::mutex> locker(m_mutex);
if (m_instance == nullptr) {
m_instance = new MyClass();
}
}
return m_instance;
}
这样处理的原因: m_instance != nullptr时表示一定被new过了; m_instance == null,表示可能被new 过,也可能没有被new过。
2. 饿汉式
饿汉式单例代码如下,线程安全的,但是不能手动灵活释放对象
static MyClass* getInstance(){
static MyClass c;
return &c;
}
3. std::call_once
std::call_once 是C++11新增功能,能够保证在多线程情况下指定函数只被调用一次;具备互斥量的能力,且比互斥量消耗的资源更少;需要与一个标记结合使用std::once_flag。
class MyClass{
public:
static MyClass* getInstance(){
std::call_once(flag, [&](){
m_instace = new MyClass;
})
return m_instance;
}
static void destory(){
if (m_instance) {
delete m_instance;
m_instance = nullptr;
}
}
private:
static MyClass* m_instance;
static std::once_flag flag;
}
MyClass* MyClass::m_instance = nullptr; //定义在.cpp中
std::once_flag MyClass::flag;
缺点是:不能实现多次创建、销毁单例, 调用 getInstance()、destory(),成功创建和销毁一次单例,再次调用 getInstance()、destory() 失败。
总结:
综合对懒汉式、饿汉式、call_once的使用方式,个人认为使用懒汉式+锁+destory的方式能解决内存泄露问题、灵活多次创建和销毁、同时也保证了线程安全。