@著作权归作者所有:来自CSDN博客作者大胡子的艾娃的原创作品,如需转载,请注明出处https://blog.csdn.net/qq_43148810,否则将追究法律责任。
如有错误的地方欢迎指正,谢谢!
一、单例应用场景
1、应用程序的日志
2、应用的配置
3、多线程的线程池
4、内存池
等等
联想:和单例相类似的有“创建后不能拷贝,只能move”,标准库中有thread、mutex、lock_guard等
二、非常经典的的两个版本
1、懒汉版本(static对象)
class Singleton
{
private:
Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
~Singleton() = default;
public:
static Singleton& getInstance()
{
static Singleton Inst;
return Inst;
}
};
2、饿汉版本(new pointer)
class Singleton
{
private:
Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
~Singleton() = default;
private:
static Singleton* m_instance;
public:
static Singleton* getInstance()
{
return m_instance;
}
};
Singleton* Singleton::m_instance = new Singleton;
三、饿汉版本(new pointer)的多线程探讨
1、多线程非安全版本
class Singleton
{
private:
Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
~Singleton() = default;
public:
static Singleton* getInstance();
private:
static Singleton* m_instance;
};
Singleton* Singleton::m_instance = nullptr;
Singleton* Singleton::getInstance()
{
if (m_instance == nullptr)
{
m_instance = new Singleton();
}
return m_instance;
}
非安全原因:多线程都同时第一次进入getInstance()函数对m_instance 进行空判,但都未创建对象,那么都会去new Singleton。
2、多线程安全版本,但锁的代价过高,不适合高并发和高频率获取
class Singleton
{
private:
Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
~Singleton() = default;
public:
static Singleton* getInstance();
private:
static std::mutex m_mutexSingleton;
static Singleton* m_instance;
};
Singleton* Singleton::m_instance = nullptr;
std::mutex Singleton::m_mutexSingleton;
Singleton* Singleton::getInstance()
{
//std::lock_guard<std::mutex> guard(m_mutexSingleton);
m_mutexSingleton.lock();
if (m_instance == nullptr)
{
m_instance = new Singleton();
}
m_mutexSingleton.unlock();
return m_instance;
}
为什么代价过高:多个线程,只要有一个线程走完锁的部分,就会new Singleton,完成“写”操作,那么后面所有线程都只有“读”操作,就不会存在线程安全问题。单例本身就只创建一个,而多次被读取,读的过程常锁带来性性能损失过大。
3、存在内存读写reorder不安全的双检查锁
除getInstance()函数和上一示例(2)不一样,其他部分都相同。
Singleton* Singleton::getInstance()
{
if (m_instance == nullptr)
{
//std::lock_guard<std::mutex> guard(m_mutexSingleton);
m_mutexSingleton.lock();
if (m_instance == nullptr)
{
m_instance = new Singleton();
}
m_mutexSingleton.unlock();
}
return m_instance;
}
表面上看解决了上一版本中加锁导致的性能问题,看起来很完美,甚至被写入教科书,但是确实存在问题。reorder会导致双检查锁失效。“m_instance = new Singleton();”这段代码由于可以被某些编译器未知因素(操作系统、硬件、其他线程等)被编译器优化。实现可能有两种情况:1、先分配内存,再调用构造去初始化这块内存,最后将m_instance指针指向这块内存;2、先分配内存,再将m_instance指针指向这这段内存,最后调用构造去初始化这块内存。reorder就是是指1和2两种不定的情况。如果是情况1是双检查锁是没有问题;但是出现情况2就会出现第一个进程走到只对m_instance分配内存并将指针指向该内存,但还未构造初始化该内存,后面的线程此时如果走到第一个检查的空判,就会返回指向未经过构造的m_instance指针。此时reorder就会导致双检查锁失效。
【备注】有的编程语言不会出现情况2,也有的非标C++有关键字(volatile)声明来处理reorder,const和指针都可以是volatile。
4、利用std::once_flag类和std::call_once函数保证创建单例只执行一次,而每次获取单例对:call_once判断。
极力推荐使用该方法
class Singleton
{
private:
Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
~Singleton() = default;
class CSingleton
{
public:
~CSingleton()
{
if (Singleton::m_instance)
{
delete Singleton::m_instance;
Singleton::m_instance = nullptr;
}
}
};
static void CreateInstance()
{
std::cout << "首次执行CreateInstance!\n";
static CSingleton CS;
m_instance = new Singleton();
}
private:
static Singleton* m_instance;
static std::once_flag m_flag;
public:
static Singleton* getInstance()
{
std::call_once(m_flag, CreateInstance);
return m_instance;
}
};
Singleton* Singleton::m_instance = nullptr;
std::once_flag Singleton::m_flag{};
5、C++11版本之后跨平台实现
std::atomic<Singleton*> Singleton::m_instance;
std::mutex Singleton::m_mutex;
Singleton* Singleton::getInstance() {
Singleton* tmp = m_instance.load(std::memory_order_relaxed);
std::atomic_thread_fence(std::memory_order_acquire);//获取内存fence
if (tmp == nullptr) {
std::lock_guard<std::mutex> lock(m_mutex);
tmp = m_instance.load(std::memory_order_relaxed);
if (tmp == nullptr) {
tmp = new Singleton;
std::atomic_thread_fence(std::memory_order_release);//释放内存fence
m_instance.store(tmp, std::memory_order_relaxed);
}
}
return tmp;
}
6、细心的会发现除4中其他的饿汉模式有new,但是没有delete,交给系统回收这段内存了,并不会产生什么问题。
通过代码回收的方式如下(未考虑多线程)。
在构造函数内创建类内静态对象CS,程序退出回收静态对象CS,利用CS的析构来释放单例。
class Singleton
{
private:
class CSingleton
{
public:
~CSingleton()
{
if (Singleton::m_instance)
{
delete Singleton::m_instance;
Singleton::m_instance = nullptr;
}
}
};
...
public:
static Singleton* getInstance()
{
static CSingleton CS;
return m_instance;
}
};
四、总结
1、只能生成一个实例的类,实现了Singleton(单例)模式的类型。
2、条件1中具体要求表现为构造函数和析构函数私有,不允许有拷贝构造和拷贝函数。
3、由静态成员函数获取Singleton(单例)对象。
4、多线程安全。
5、若为饿汉式,创建单例对象后,不存在线程安全问题,考虑的性能问题。
6、reorder问题会导致经典双检查锁C++实现失效。
如有错误或不足欢迎评论指出!创作不易,转载请注明出处。如有帮助,记得点赞关注哦(⊙o⊙)
更多内容请关注个人博客:https://blog.csdn.net/qq_43148810