本文系原创,转载请注明:http://www.cnblogs.com/inevermore/p/4014577.html

 

根据维基百科,对单例模式的描述是:

确保一个类只有一个实例,并提供对该实例的全局访问。

从这段话,我们可以得出单例模式的最重要特点:

一个类最多只有一个对象

 

单线程环境

 

对于一个普通的类,我们可以任意的生成对象,所以我们为了避免生成太多的类,需要将类的构造函数设置为私有。

所以我们写出第一步:

  1. class Singleton
  2. {
  3. public:
  4.  
  5. private:
  6. Singleton() { }
  7. };

此时在main中就无法直接生成对象:

  1. Singleton s; //ERROR

那么我们想要获取实例,只能借助于类内部的函数,于是我们添加一个内部的函数,而且必须是static函数(思考为什么):

  1. class Singleton
  2. {
  3. public:
  4. static Singleton *getInstance()
  5. {
  6. return new Singleton;
  7. }
  8. private:
  9. Singleton() { }
  10. };

OK,我们可以用这个函数生成对象了,但是每次都去new,无法保证唯一性,于是我们将对象保存在一个static指针内,然后每次获取对象时,先检查该指针是否为空:

  1. class Singleton
  2. {
  3. public:
  4. static Singleton *getInstance()
  5. {
  6. if(pInstance_ == NULL) //线程的切换
  7. {
  8. ::sleep(1);
  9. pInstance_ = new Singleton;
  10. }
  11.  
  12. return pInstance_;
  13. }
  14. private:
  15. Singleton() { }
  16.  
  17. static Singleton *pInstance_;
  18. };
  19.  
  20. Singleton *Singleton::pInstance_ = NULL;

我们在main中测试:

  1. cout << Singleton::getInstance() << endl;
  2. cout << Singleton::getInstance() << endl;

可以看到生成了相同的对象,单例模式编写初步成功。

 

多线程环境下的考虑

 

但是目前的代码就真的没问题了吗?

我写出了以下的测试:

  1. class TestThread : public Thread
  2. {
  3. public:
  4. void run()
  5. {
  6. cout << Singleton::getInstance() << endl;
  7. cout << Singleton::getInstance() << endl;
  8. }
  9. };
  10.  
  11. int main(int argc, char const *argv[])
  12. {
  13. //测试证明了多线程下本代码存在竞争问题
  14.  
  15. TestThread threads[12];
  16. for(int ix = 0; ix != 12; ++ix)
  17. {
  18. threads[ix].start();
  19. }
  20.  
  21. for(int ix = 0; ix != 12; ++ix)
  22. {
  23. threads[ix].join();
  24. }
  25. return 0;
  26. }

 

这里注意,为了达到效果,我特意做了如下改动:

  1. if(pInstance_ == NULL) //线程的切换
  2. {
  3. ::sleep(1);
  4. pInstance_ = new Singleton;
  5. }

这样故意造成线程的切换

打印结果如下:

  1. 0xb1300468
  2. 0xb1300498
  3. 0x9f88728
  4. 0xb1300498
  5. 0xb1300478
  6. 0xb1300498
  7. 0xb1100488
  8. 0xb1300498
  9. 0xb1300488
  10. 0xb1300498
  11. 0xb1300498
  12. 0xb1300498
  13. 0x9f88738
  14. 0xb1300498
  15. 0x9f88748
  16. 0xb1300498
  17. 0xb1100478
  18. 0xb1300498
  19. 0xb1100498
  20. 0xb1300498
  21. 0xb1100468
  22. 0xb1300498
  23. 0xb11004a8
  24. 0xb11004a8

 

很显然,我们的代码在多线程下经不起推敲。

怎么办?加锁! 于是我们再度改进:

  1. class Singleton
  2. {
  3. public:
  4. static Singleton *getInstance()
  5. {
  6. mutex_.lock();
  7. if(pInstance_ == NULL) //线程的切换
  8. pInstance_ = new Singleton;
  9. mutex_.unlock();
  10. return pInstance_;
  11. }
  12. private:
  13. Singleton() { }
  14.  
  15. static Singleton *pInstance_;
  16. static MutexLock mutex_;
  17. };
  18.  
  19. Singleton *Singleton::pInstance_ = NULL;
  20. MutexLock Singleton::mutex_;

此时测试,无问题。

但是,互斥锁会极大的降低系统的并发能力,因为每次调用都要加锁,等于一群人过独木桥

我写了一份测试如下:

  1. class TestThread : public Thread
  2. {
  3. public:
  4. void run()
  5. {
  6. const int kCount = 1000 * 1000;
  7. for(int ix = 0; ix != kCount; ++ix)
  8. {
  9. Singleton::getInstance();
  10. }
  11. }
  12. };
  13.  
  14. int main(int argc, char const *argv[])
  15. {
  16. //Singleton s; ERROR
  17.  
  18. int64_t startTime = getUTime();
  19.  
  20. const int KSize = 100;
  21. TestThread threads[KSize];
  22. for(int ix = 0; ix != KSize; ++ix)
  23. {
  24. threads[ix].start();
  25. }
  26.  
  27. for(int ix = 0; ix != KSize; ++ix)
  28. {
  29. threads[ix].join();
  30. }
  31.  
  32. int64_t endTime = getUTime();
  33.  
  34. int64_t diffTime = endTime - startTime;
  35. cout << "cost : " << diffTime / 1000 << " ms" << endl;
  36.  
  37. return 0;
  38. }

开了100个线程,每个调用1M次getInstance,其中getUtime的定义如下:

  1. int64_t getUTime()
  2. {
  3. struct timeval tv;
  4. ::memset(&tv, 0, sizeof tv);
  5. if(gettimeofday(&tv, NULL) == -1)
  6. {
  7. perror("gettimeofday");
  8. exit(EXIT_FAILURE);
  9. }
  10. int64_t current = tv.tv_usec;
  11. current += tv.tv_sec * 1000 * 1000;
  12. return current;
  13. }

运行结果为:

  1. cost : 6914 ms

 

 

采用双重锁模式

 

上面的测试,我们还无法看出性能问题,我再次改进代码:

  1. class Singleton
  2. {
  3. public:
  4. static Singleton *getInstance()
  5. {
  6. if(pInstance_ == NULL)
  7. {
  8. mutex_.lock();
  9. if(pInstance_ == NULL) //线程的切换
  10. pInstance_ = new Singleton;
  11. mutex_.unlock();
  12. }
  13.  
  14. return pInstance_;
  15. }
  16. private:
  17. Singleton() { }
  18.  
  19. static Singleton *pInstance_;
  20. static MutexLock mutex_;
  21. };
  22.  
  23. Singleton *Singleton::pInstance_ = NULL;
  24. MutexLock Singleton::mutex_;

可以看到,我在getInstance中采用了两重检查模式,这段代码的优点体现在哪里?

内部采用互斥锁,代码无论如何是可靠的

new出第一个实例后,后面每个线程访问到最外面的if判断就直接返回了,没有加锁的开销

我再次运行测试,(测试代码不变),结果如下:

  1. cost : 438 ms

啊哈,十几倍的性能差距,可见我们的改进是有效的,仅仅三行代码,却带来了十几倍的效率提升!

 

尾声

 

上面这种编写方式成为DCLP(Double-Check-Locking-Pattern)模式,这种方式一度被认为是绝对正确的,但是后来有人指出这种方式在某些情况下也会乱序执行,可以参考Scott的C++ and the Perils of Double-Checked Locking - Scott Meyer

C++Singleton的DCLP(双重锁)实现以及性能测评的更多相关文章

  1. volatile双重锁实现单例

    双重锁实现单例时遭到质疑,既是:双重锁也无法保证单例模式! 原因是:指令会重排序,普通的变量仅仅会保证该方法在执行时,所有依赖的赋值结果是正确的,但不会保证执行顺序! 为什么会重排序:指令重排序是指c ...

  2. 单例模式-DCL双重锁检查实现及原理刨析

    以我的经验为例(如有不对欢迎指正),在生产过程中,经常会遇到下面两种情况: 1.封装的某个类不包含具有具体业务含义的类成员变量,是对业务动作的封装,如MVC中的各层(HTTPRequest对象以Thr ...

  3. 单例模式的双重锁为什么要加volatile(转)

    单例模式如下: 需要volatile关键字的原因是,在并发情况下,如果没有volatile关键字,在第5行会出现问题. instance = new TestInstance();可以分解为3行伪代码 ...

  4. 单例模式(Singleton)的同步锁synchronized

    单例模式,有“懒汉式”和“饿汉式”两种. 懒汉式 单例类的实例在第一次被引用时候才被初始化. public class Singleton { private static Singleton ins ...

  5. Qt中实现单例模式(SingleTon),大约有3种办法

    Qt中实现单例模式(SingleTon) 单例模式分为“饥汉”和“饿汉”两种版本,也正是线程安全问题使得原本简单的单例模式变得复杂.由于单例模式很常用,Boost库中有强大的泛型单例实现,我也利用Qt ...

  6. java常见设计模式

    工厂模式 普通工厂模式,就是建立一个工厂类,对实现了同一接口的一些类进行实例的创建. 多个工厂模式,编写多个创建工厂的方法即可. 静态工厂模式,在多个工厂模式的基础上把Factory种方法的返回值标明 ...

  7. 单例模式双重检验锁的判断是否为null的意义

    关于双重检验锁首先简单来看一个小例子: public class Singleton{ private static Singleton instance = null; private Single ...

  8. 双重校验锁 --使用volatile和两次判空校验

    介绍 双重校验锁是单例模式中,饿汉式的一种实现方式.因为有两次判空校验,所以叫双重校验锁,一次是在同步代码块外,一次是在同步代码块内. 为什么在同步代码块内还要再检验一次? 第一个if减少性能开销,第 ...

  9. Singleton(单例模式)

    一. /** * lazy man(不是线程安全的) * @author TMAC-J * */ public class Singleton { private static Singleton i ...

随机推荐

  1. Hibernate中映射一对一关联(按主键映射和外键映射)和组件映射

                                                        Hibernate中映射一对一关联(按主键映射和外键映射)和组件映射 Hibernate提供了两 ...

  2. 使用 padding-bottom 设置高度基于宽度的自适应

    我们在做移动端列表,通常会做到图文列表,列表是自适应的.当列表中有图片,图片的宽度是随着列表宽的变化而变化,我们为了在图片宽度变化的时候做到图片的不变形,所有采用以下办法. 本文章只讲语法 html ...

  3. STM in Haskell

    Software Transactional Memory,软件事务内存管理(应该是这么翻译的吧T_T) 类似于数据库的事务,所有的操作都有log,最后验证其他线程是否对数据进行修改,要是有那么就回滚 ...

  4. xCode中去除“Implicit declaration of function 'sysctl' is invalid in C99” 警告

    http://blog.csdn.net/dreambegin/article/details/8609121 一般出现该问题是因为通过C调用了unix/linux 底层接口,所以需要调整c语言的编译 ...

  5. screenshoter 連續截圖工具

    https://pcrookie.com/?p=993 顯示 mouse 設定 Settings -> Saving -> Display mouse cursor

  6. hdu 5166(水题)

    Missing number Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 65536/65536 K (Java/Others)To ...

  7. 计蒜客 28449.算个欧拉函数给大家助助兴-大数的因子个数 (HDU5649.DZY Loves Sorting) ( ACM训练联盟周赛 G)

    ACM训练联盟周赛 这一场有几个数据结构的题,但是自己太菜,不会树套树,带插入的区间第K小-替罪羊套函数式线段树, 先立个flag,BZOJ3065: 带插入区间K小值 计蒜客 Zeratul与Xor ...

  8. HDU 5732 Subway(2016多校1J,树的重心 + 哈希)

    题目链接  2016多校1 Problem J 题意  给定两棵相同的树,但是编号方案不同.求第一棵树上的每个点对应的第二棵树上的点.输出一种方案即可. 首先确定树的直径的中点.两棵树相等意味着两棵树 ...

  9. 前端中 width 的获取

    这篇文章其实是在了解 viewport 的过程中发现这些概念容易混淆做了个小小的总结.viewport的首要关键是宽度的获取,宽度的计算有下面几个属性和方法: clientWidth offsetWi ...

  10. linux之vim命令

    :tabe fn     在一个新的标签页中编辑文件fngt     切换到下一个标签页gT     切换到上一个标签页:tabr     切换到第一个标签页:tabl     切换到最后一个标签页: ...