相信很多小伙伴,对单例模式很熟悉,但是对于选择哪一种单例模式方案,可能不是特别清楚。

对网上五花百门的实现方式,是不是觉得很头大,到底这些方案都有些啥缺点,啥优点,哪种最完美,可以作为自己的常用代码库。

如果有耐心,请仔细阅读下文,带你回顾一下程序员对于单例模式实现方案的辛酸历程。

没有耐心的话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(),是一个写操作,前面有一个无锁的读操作。当真正的写操作进行时,前面的读操作存在脏读情况。

另外其他原因:https://blog.csdn.net/flyingleo1981/article/details/45485293?depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromBaidu-1&utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromBaidu-1

这对于码农来说,经过一番折腾,然而却没有得到一个完美的解决方案,这是残忍的。。。

那么,有没有线程安全的懒汉模式单例实现方案呢?答案是有,还好有你(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实现线程安全的单例模式相关推荐

  1. 静态内部类、静态变量的加载次数-理解静态内部类实现线程安全的单例模式

    百度众说纷纭的情况下就不如自己写例子测试理论,话不多说,上代码: public class Sta {public static long date=System.currentTimeMillis( ...

  2. 静态内部类、静态变量的加载次数-理解静态内部类实现线程安全的单例模式懒加载

    原贴:https://www.cnblogs.com/suncoolcat/p/3362230.html 写的很详细,需细细品味. 百度众说纷纭的情况下就不如自己写例子测试理论,话不多说,上代码: p ...

  3. C++ static 变量和线程安全和单例模式

    static变量初始化顺序 1.1 全局变量.文件域的static变量和类的static成员变量在main函数执行之前初始化 1.2 局部静态变量在第一次被使用时初始化 static变量的线程安全 2 ...

  4. Android之线程安全的单例模式,Adapter注意事项之引用传值

    线程安全的单例模式 单位模式一般写法如下: public static FestivalLab mInstance; private FestivalLab() { } public static F ...

  5. [转]C++ 11 多线程--线程管理

    转载地址:https://www.cnblogs.com/wangguchangqing/p/6134635.html 说到多线程编程,那么就不得不提并行和并发,多线程是实现并发(并行)的一种手段.并 ...

  6. java 静态内部类 线程安全问题_单例模式的七种写法, 面试题:线程安全的单例模式...

    http://cantellow.iteye.com/blog/838473 http://meizhi.iteye.com/blog/537563 第一种(懒汉,线程不安全): Java代码  pu ...

  7. 【JUC并发编程11】线程池

    文章目录 线程池 11.1 线程池概述 11.2 线程池架构 11.3 线程池使用方式 11.4 线程池底层原则 11.5 线程池的七个参数 11.6 线程池底层工作流程 11.7 自定义线程池 线程 ...

  8. C++ 11 多线程--线程管理

    说到多线程编程,那么就不得不提并行和并发,多线程是实现并发(并行)的一种手段.并行是指两个或多个独立的操作同时进行.注意这里是同时进行,区别于并发,在一个时间段内执行多个操作.在单核时代,多个线程是并 ...

  9. C++实现线程安全的单例模式

    C++实现线程安全的单例模式 1.单例模式 一个简单的单例模式很容易实现:构造函数声明为private或protect防止被外部函数实例化,内部保存一个private static的类指针保存唯一的实 ...

最新文章

  1. 第十九课.基于sklearn的SVM人脸识别
  2. 自定义view-波纹扩散(圆扩散)
  3. MonkeyRunner在Windows下的Eclipse开发环境搭建步骤(兼解决网上Jython配置出错的问题)...
  4. Boost:异步操作,需要boost :: asio :: async_compose函数的测试程序
  5. 双11奇迹背后的大数据平台,不喧哗,自有声!
  6. 全网最细之N种内部类详细讲解
  7. python热键+鼠标键盘控制
  8. linux时间与日期函数,Shell中关于时间和日期的函数总结
  9. Flink的容错机制
  10. linux如何获取raw中的文件路径,如何使用Linux获得Touchscreen Rawdata的坐标
  11. 为什么算法工程师也叫调参狗?
  12. eclipse打包java_eclipse实现将Java项目打包成jar包
  13. ICEM 网格划分技巧(21-5)
  14. c语言打砖块小游戏,C语言动画(打砖块游戏)
  15. Android Parcel对象详解
  16. 2021-2027全球与中国卸扣式绝缘子市场现状及未来发展趋势
  17. iOS生成gif图片
  18. 2022年6月语音合成(TTS)和语音识别(ASR)论文月报
  19. RMAN Recipes 中对Flash Recovery Area的总结
  20. 关于内存条低压标压以及不同品牌容量内存条组建双通道笔记

热门文章

  1. 计算机操作系统对文件进行管理的体现,计算机操作系统复习之文件管理
  2. 开发工具和DBeaver,连接多种数据库
  3. 如何快速构建用户画像?
  4. Windows11清理C盘空间优化
  5. 项目启动会发言稿(范文三)
  6. Ubuntu下python3卸载
  7. 蓝桥杯 算法提高 聪明的美食家
  8. Ubuntu安装Ipopt过程
  9. j2ee入门到精通学习步骤
  10. 东北大学acm暑期夏令营结构体