使用C++11实现线程安全的单例模式
相信很多小伙伴,对单例模式很熟悉,但是对于选择哪一种单例模式方案,可能不是特别清楚。
对网上五花百门的实现方式,是不是觉得很头大,到底这些方案都有些啥缺点,啥优点,哪种最完美,可以作为自己的常用代码库。
如果有耐心,请仔细阅读下文,带你回顾一下程序员对于单例模式实现方案的辛酸历程。
没有耐心的话o(*^@^*)o,请直接跳到《C++11实现线程安全单例》章节,你将得到一份完美的多线程安全单例模式代码。
后面我会把完整代码贴出来,供下载。
1、C++11前,程序员们是怎么实现单例模式?
(1)懒汉模式与饿汉模式
首先来了解一下懒汉模式与饿汉模式。
懒汉模式,顾名思义,就是比较懒惰,不是今天必须干的事,坚决放到明天来完成,带有延迟加载的意思。
饿汉模式,意思就是饿了的流浪汉,这种流浪汉什么事情都可以干的,但凡路边的垃圾,街上的妹纸,他都可以吃得下去,饥不择食;
软件一起来,就尽早吃内存,带有提前加载的意思(哪怕暂时用不到)。
我们举一个栗子,比如键盘,一个系统正常输入,我只需要一个键盘,就可以了,所以键盘设计为一个单例,有个打字方法writeWords()。
饿汉模式代码:
class Keyboard
{
public:Keyboard() {}~Keyboard() {}static Keyboard* instance(){return _pInstance;}void writeWords() { }private:static Keyboard* _pInstance;
};Keyboard* Keyboard::_pInstance = new Keyboard();
懒汉模式代码:
class Keyboard
{
public:Keyboard() {}~Keyboard() {}static Keyboard* instance(){if (!_pInstance){_pInstance = new Keyboard();}return _pInstance;}void writeWords() { }private:static Keyboard* _pInstance;
};Keyboard* Keyboard::_pInstance = NULL;
(2)禁止构造函数、拷贝构造与赋值函数
既然是单例,肯定不允许外面调用构造函数实例化新对象;也不允许拷贝间接实例化新对象;也不允许对象赋值。
我们改造下上面的懒汉与饿汉。
饿汉模式代码:
class Keyboard
{
private:Keyboard() = default;~Keyboard() = default;Keyboard(const Keyboard&)=delete;Keyboard& operator=(const Keyboard&)=delete;public:static Keyboard* instance(){return _pInstance;}void writeWords() { }private:static Keyboard* _pInstance;
};Keyboard* Keyboard::_pInstance = new Keyboard();
懒汉模式代码:
class Keyboard
{
private:Keyboard() = default;~Keyboard() = default;Keyboard(const Keyboard&)=delete;Keyboard& operator=(const Keyboard&)=delete;public:static Keyboard* instance(){if (!_pInstance){_pInstance = new Keyboard();}return _pInstance;}void writeWords() { }private:static Keyboard* _pInstance;
};Keyboard* Keyboard::_pInstance = NULL;
(3)单例的模板化
现在我们再来举个栗子,现在已有一个键盘单例了,像这样的单例,我们还需要很多个,比如鼠标、显示器、耳机。。。(请忽略合理性)。
假设我们选择懒汉模式,那么我们还需要分别添加Mouse、Displayer、Headset三个类,且其实现单例功能的代码和Keyboard高度类似。
发现了吗,小伙伴,我们在做重复性工作了,so,我们需要将单例类模板化,这样每个不同的类需要实现单例,可以套用一个模板。
饿汉模式代码:
template <class T>
class Singleton
{
private:Singleton() = default;~Singleton() = default;Singleton(const Singleton&)=delete;Singleton& operator=(const Singleton&)=delete;public:static T* instance() { return _instance; }private:static T* _instance;
};template <class T>
T* Singleton<T>::_instance = new T();
懒汉模式代码:
template <class T>
class Singleton
{
private:Singleton() = default;~Singleton() = default;Singleton(const Singleton&)=delete;Singleton& operator=(const Singleton&)=delete;public:static T* instance(){if (!_instance){_instance = new T();}return _instance;}private:static T* _instance;
};template <class T>
T* Singleton<T>::_instance = NULL;
到此,饿汉模式就是最终版本了,后续不会对它进行改造了。
饿汉模式的缺点:也是比较明显,不使用却占用资源,不支持向单例构造函数传参;
优点:就是节省了运行时间(资源提前加载),另外天生自带线程安全属性(在多线程环境下肯定是线程安全的,因为不存在多线程实例化的问题)。
(4)懒汉模式之线程安全性探索
a.懒汉模式下,在定义_instance 变量时先等于NULL,在调用instance()方法时,再判断是否要赋值。这种模式,并非是线程安全的,因为多个线程同时调用instance()方法,就可能导致有产生多个实例。比如A线程执行到第13行之后,第15行之前,当前_instance==NULL,此时由于线程调度,切到B线程,B线程发现_instance==NULL,则进入new T()进行实例化,实例化完成,返回对象指针,然后某一刻发生线程调度,切回到A线程,A线程从以前被打断的地方继续执行,发现_instance==NULL,则进入new T()进行实例化,这样就出现了2个单例对象。显然这是线程非安全的。
那么,要实现线程安全,就必须加锁,以保证对象实例化的原子性。
改造后的代码:
template <class T>
class Singleton
{
private:Singleton() = default;~Singleton() = default;Singleton(const Singleton&)=delete;Singleton& operator=(const Singleton&)=delete;public:static T* instance(){std::lock_guard<std::mutex> lock_(m_cs);if (!_instance){_instance = new T();}return _instance;}private:static T* _instance;static std::mutex m_cs;
};template <class T>
T* Singleton<T>::_instance = NULL; template <class T>
std::mutex Singleton<T>::m_cs;
似乎解决了多线程实例化安全性问题,完美?
但是似乎引出了其他的问题,我们在每次调用instance()时,都会调用进一次加/解锁,但是实际上这个锁只在我们第一次创建对象时,用来防止多线程竞争起到作用。对象创建起来后,多线程都是读取操作,没有写入操作,所以就不会有安全性问题,此后的调用,我们无疑浪费了很多资源。
b.此时我们需要引出一个高大上的名称:DCLP(Double-Checked Locking Pattern),即“双检锁”。怎么操作?就是加个if判断。
改造后的代码:
template <class T>
class Singleton
{
private:Singleton() = default;~Singleton() = default;Singleton(const Singleton&)=delete;Singleton& operator=(const Singleton&)=delete;public:static T* instance(){if (!_instance){std::lock_guard<std::mutex> lock_(m_cs);if (!_instance){_instance = new T();}}return _instance;}private:static T* _instance;static std::mutex m_cs;
};template <class T>
T* Singleton<T>::_instance = NULL; template <class T>
std::mutex Singleton<T>::m_cs;
这样就安全了吗。细想下其实还是不安全的。
注意到_instance = new T(),是一个写操作,前面有一个无锁的读操作。当真正的写操作进行时,前面的读操作存在脏读情况。
这对于码农来说,经过一番折腾,然而却没有得到一个完美的解决方案,这是残忍的。。。
那么,有没有线程安全的懒汉模式单例实现方案呢?答案是有,还好有你(C++11)。下一节介绍C++11实现线程安全单例。
2、C++11实现线程安全单例(懒汉)
在C++11中提供一种方法,使得函数可以线程安全的只调用一次。即使用std::call_once和std::once_flag。std::call_once是一种lazy load的很简单易用的机制。
懒汉模式改造后的代码:
template <class T>
class Singleton
{
private:Singleton() = default;~Singleton() = default;Singleton(const Singleton&)=delete;Singleton& operator=(const Singleton&)=delete;public:static T* instance(){std::call_once(_flag, [&](){_instance = new T();});return _instance;}private:static T* _instance;static std::once_flag _flag;
};template <class T>
T* Singleton<T>::_instance = NULL; template <class T>
std::once_flag Singleton<T>::_flag;
之前我们的单例,instance()只能创建默认构造函数的对象,但是有时候需要给单例传递参数,那么我们需要对instance()方法进行改造,在c++11中,已经支持了可变参数函数。
然而向单例构造函数中传参,这个需求,饿汉模式就无法实现了。
下面我们继续改造,添加构造函数传参。
懒汉模式改造后的代码:
template <class T>
class Singleton
{
private:Singleton() = default;~Singleton() = default;Singleton(const Singleton&)=delete;Singleton& operator=(const Singleton&)=delete;public:template <typename... Args>static T* instance(Args&&... args){std::call_once(_flag, &](){_instance = new T(std::forward<Args>(args)...);});return _instance;}private:static T* _instance;static std::once_flag _flag;
};template <class T>
T* Singleton<T>::_instance = NULL; template <class T>
std::once_flag Singleton<T>::_flag;
一般而言,单例对象无需手动释放,程序结束后,由操作系统自动回收资源。但是为了某些时候特殊处理,我们还是添加上destroy()方法。
改造后的代码:
template <class T>
class Singleton
{
private:Singleton() = default;~Singleton() = default;Singleton(const Singleton&)=delete;Singleton& operator=(const Singleton&)=delete;public:template <typename... Args>static T* instance(Args&&... args){std::call_once(_flag, [&](){_instance = new T(std::forward<Args>(args)...);});return _instance;}static void destroy(){if (_instance){delete _instance;_instance = NULL;}}private:static T* _instance;static std::once_flag _flag;
};template <class T>
T* Singleton<T>::_instance = NULL; template <class T>
std::once_flag Singleton<T>::_flag;
到此,这里为懒汉模式的最终版,我们姑且称之为懒汉版本1。
在C++11标准中,要求局部静态变量初始化具有线程安全性。
另外还有一个版本的懒汉模式代码,也是支持线程安全(打开编译器C++11支持),大家看看,大概长这样:
class Singleton
{
public:static Singleton* instance(){static Singleton _instance;return &s_instance;}
private:Singleton() {}
};
这里使用了局部静态变量,C++11机制可以保证它的初始化具有原子性,线程安全。
这个对象保存在静态数据区,和全局变量是在一起的,而不是在堆中。
习惯让我感觉对象保存在堆中更好,至于是不是,待解释。
姑且称这个为懒汉版本2。
3、结论(拿干货)
经过上面的角逐,现在剩下3位选手:饿汉最终版、懒汉版本1、懒汉版本2。
饿汉与懒汉阵营PK:饿汉资源提前加载,浪费比较严重,尤其有一些功能,用户可以选择性启用的,用户如果不需要,
犯不着一上来就占用额外资源;懒汉不存在资源浪费,且同时具备线程安全。
第一局:懒汉胜利,选择懒汉。
个人觉得还是推荐使用懒汉模式,支持线程安全,构造函数传参,手动回收资源。
第二局:懒汉版本1 PK 版本2。
套用上面一句话:习惯让我感觉对象保存在堆中更好,至于是不是,待解释。
所以我们选择懒汉版本1。真是玩笑了,O(∩_∩)O哈哈~
具体用哪个懒汉,看个人喜好吧。
我个人比较倾向于懒汉版本1。
下面是测试代码(main.cpp):
#include <QCoreApplication>
#include <iostream>
#include "singleton.h"class Keyboard
{
public:Keyboard(int a = 0, float b = 0.0){std::cout << "Keyboard():" << (a+b) << std::endl;}~Keyboard(){std::cout << "~Keyboard()" << std::endl;}void writeWords(){std::cout << "I'm writing! addr : " << (int)this << std::endl;}
};int main(int argc, char *argv[])
{Keyboard* t1 = Singleton<Keyboard>::instance(5, 2.0);Keyboard* t2 = Singleton<Keyboard>::instance(6, 5.0);t1->writeWords();t2->writeWords();Singleton<Keyboard>::destroy();QCoreApplication a(argc, argv);return a.exec();
}
结果:
C++11实现线程安全单例代码和测试代码,下载地址:
https://download.csdn.net/download/u011832525/12306370
若对你有帮助,欢迎点赞、收藏、评论,你的支持就是我的最大动力!!!
同时,阿超为大家准备了丰富的学习资料,欢迎关注公众号“超哥学编程”,即可领取。
本文涉及工程代码,公众号回复:Singleton,即可下载。
使用C++11实现线程安全的单例模式相关推荐
- 静态内部类、静态变量的加载次数-理解静态内部类实现线程安全的单例模式
百度众说纷纭的情况下就不如自己写例子测试理论,话不多说,上代码: public class Sta {public static long date=System.currentTimeMillis( ...
- 静态内部类、静态变量的加载次数-理解静态内部类实现线程安全的单例模式懒加载
原贴:https://www.cnblogs.com/suncoolcat/p/3362230.html 写的很详细,需细细品味. 百度众说纷纭的情况下就不如自己写例子测试理论,话不多说,上代码: p ...
- C++ static 变量和线程安全和单例模式
static变量初始化顺序 1.1 全局变量.文件域的static变量和类的static成员变量在main函数执行之前初始化 1.2 局部静态变量在第一次被使用时初始化 static变量的线程安全 2 ...
- Android之线程安全的单例模式,Adapter注意事项之引用传值
线程安全的单例模式 单位模式一般写法如下: public static FestivalLab mInstance; private FestivalLab() { } public static F ...
- [转]C++ 11 多线程--线程管理
转载地址:https://www.cnblogs.com/wangguchangqing/p/6134635.html 说到多线程编程,那么就不得不提并行和并发,多线程是实现并发(并行)的一种手段.并 ...
- java 静态内部类 线程安全问题_单例模式的七种写法, 面试题:线程安全的单例模式...
http://cantellow.iteye.com/blog/838473 http://meizhi.iteye.com/blog/537563 第一种(懒汉,线程不安全): Java代码 pu ...
- 【JUC并发编程11】线程池
文章目录 线程池 11.1 线程池概述 11.2 线程池架构 11.3 线程池使用方式 11.4 线程池底层原则 11.5 线程池的七个参数 11.6 线程池底层工作流程 11.7 自定义线程池 线程 ...
- C++ 11 多线程--线程管理
说到多线程编程,那么就不得不提并行和并发,多线程是实现并发(并行)的一种手段.并行是指两个或多个独立的操作同时进行.注意这里是同时进行,区别于并发,在一个时间段内执行多个操作.在单核时代,多个线程是并 ...
- C++实现线程安全的单例模式
C++实现线程安全的单例模式 1.单例模式 一个简单的单例模式很容易实现:构造函数声明为private或protect防止被外部函数实例化,内部保存一个private static的类指针保存唯一的实 ...
最新文章
- 第十九课.基于sklearn的SVM人脸识别
- 自定义view-波纹扩散(圆扩散)
- MonkeyRunner在Windows下的Eclipse开发环境搭建步骤(兼解决网上Jython配置出错的问题)...
- Boost:异步操作,需要boost :: asio :: async_compose函数的测试程序
- 双11奇迹背后的大数据平台,不喧哗,自有声!
- 全网最细之N种内部类详细讲解
- python热键+鼠标键盘控制
- linux时间与日期函数,Shell中关于时间和日期的函数总结
- Flink的容错机制
- linux如何获取raw中的文件路径,如何使用Linux获得Touchscreen Rawdata的坐标
- 为什么算法工程师也叫调参狗?
- eclipse打包java_eclipse实现将Java项目打包成jar包
- ICEM 网格划分技巧(21-5)
- c语言打砖块小游戏,C语言动画(打砖块游戏)
- Android Parcel对象详解
- 2021-2027全球与中国卸扣式绝缘子市场现状及未来发展趋势
- iOS生成gif图片
- 2022年6月语音合成(TTS)和语音识别(ASR)论文月报
- RMAN Recipes 中对Flash Recovery Area的总结
- 关于内存条低压标压以及不同品牌容量内存条组建双通道笔记