C++Singleton的DCLP(双重锁)实现以及性能测评
本文系原创,转载请注明:http://www.cnblogs.com/inevermore/p/4014577.html
根据维基百科,对单例模式的描述是:
确保一个类只有一个实例,并提供对该实例的全局访问。
从这段话,我们可以得出单例模式的最重要特点:
一个类最多只有一个对象
单线程环境
对于一个普通的类,我们可以任意的生成对象,所以我们为了避免生成太多的类,需要将类的构造函数设置为私有。
所以我们写出第一步:
- class Singleton
- {
- public:
- private:
- Singleton() { }
- };
此时在main中就无法直接生成对象:
- Singleton s; //ERROR
那么我们想要获取实例,只能借助于类内部的函数,于是我们添加一个内部的函数,而且必须是static函数(思考为什么):
- class Singleton
- {
- public:
- static Singleton *getInstance()
- {
- return new Singleton;
- }
- private:
- Singleton() { }
- };
OK,我们可以用这个函数生成对象了,但是每次都去new,无法保证唯一性,于是我们将对象保存在一个static指针内,然后每次获取对象时,先检查该指针是否为空:
- class Singleton
- {
- public:
- static Singleton *getInstance()
- {
- if(pInstance_ == NULL) //线程的切换
- {
- ::sleep(1);
- pInstance_ = new Singleton;
- }
- return pInstance_;
- }
- private:
- Singleton() { }
- static Singleton *pInstance_;
- };
- Singleton *Singleton::pInstance_ = NULL;
我们在main中测试:
- cout << Singleton::getInstance() << endl;
- cout << Singleton::getInstance() << endl;
可以看到生成了相同的对象,单例模式编写初步成功。
多线程环境下的考虑
但是目前的代码就真的没问题了吗?
我写出了以下的测试:
- class TestThread : public Thread
- {
- public:
- void run()
- {
- cout << Singleton::getInstance() << endl;
- cout << Singleton::getInstance() << endl;
- }
- };
- int main(int argc, char const *argv[])
- {
- //测试证明了多线程下本代码存在竞争问题
- TestThread threads[12];
- for(int ix = 0; ix != 12; ++ix)
- {
- threads[ix].start();
- }
- for(int ix = 0; ix != 12; ++ix)
- {
- threads[ix].join();
- }
- return 0;
- }
这里注意,为了达到效果,我特意做了如下改动:
- if(pInstance_ == NULL) //线程的切换
- {
- ::sleep(1);
- pInstance_ = new Singleton;
- }
这样故意造成线程的切换。
打印结果如下:
- 0xb1300468
- 0xb1300498
- 0x9f88728
- 0xb1300498
- 0xb1300478
- 0xb1300498
- 0xb1100488
- 0xb1300498
- 0xb1300488
- 0xb1300498
- 0xb1300498
- 0xb1300498
- 0x9f88738
- 0xb1300498
- 0x9f88748
- 0xb1300498
- 0xb1100478
- 0xb1300498
- 0xb1100498
- 0xb1300498
- 0xb1100468
- 0xb1300498
- 0xb11004a8
- 0xb11004a8
很显然,我们的代码在多线程下经不起推敲。
怎么办?加锁! 于是我们再度改进:
- class Singleton
- {
- public:
- static Singleton *getInstance()
- {
- mutex_.lock();
- if(pInstance_ == NULL) //线程的切换
- pInstance_ = new Singleton;
- mutex_.unlock();
- return pInstance_;
- }
- private:
- Singleton() { }
- static Singleton *pInstance_;
- static MutexLock mutex_;
- };
- Singleton *Singleton::pInstance_ = NULL;
- MutexLock Singleton::mutex_;
此时测试,无问题。
但是,互斥锁会极大的降低系统的并发能力,因为每次调用都要加锁,等于一群人过独木桥。
我写了一份测试如下:
- class TestThread : public Thread
- {
- public:
- void run()
- {
- const int kCount = 1000 * 1000;
- for(int ix = 0; ix != kCount; ++ix)
- {
- Singleton::getInstance();
- }
- }
- };
- int main(int argc, char const *argv[])
- {
- //Singleton s; ERROR
- int64_t startTime = getUTime();
- const int KSize = 100;
- TestThread threads[KSize];
- for(int ix = 0; ix != KSize; ++ix)
- {
- threads[ix].start();
- }
- for(int ix = 0; ix != KSize; ++ix)
- {
- threads[ix].join();
- }
- int64_t endTime = getUTime();
- int64_t diffTime = endTime - startTime;
- cout << "cost : " << diffTime / 1000 << " ms" << endl;
- return 0;
- }
开了100个线程,每个调用1M次getInstance,其中getUtime的定义如下:
- int64_t getUTime()
- {
- struct timeval tv;
- ::memset(&tv, 0, sizeof tv);
- if(gettimeofday(&tv, NULL) == -1)
- {
- perror("gettimeofday");
- exit(EXIT_FAILURE);
- }
- int64_t current = tv.tv_usec;
- current += tv.tv_sec * 1000 * 1000;
- return current;
- }
运行结果为:
- cost : 6914 ms
采用双重锁模式
上面的测试,我们还无法看出性能问题,我再次改进代码:
- class Singleton
- {
- public:
- static Singleton *getInstance()
- {
- if(pInstance_ == NULL)
- {
- mutex_.lock();
- if(pInstance_ == NULL) //线程的切换
- pInstance_ = new Singleton;
- mutex_.unlock();
- }
- return pInstance_;
- }
- private:
- Singleton() { }
- static Singleton *pInstance_;
- static MutexLock mutex_;
- };
- Singleton *Singleton::pInstance_ = NULL;
- MutexLock Singleton::mutex_;
可以看到,我在getInstance中采用了两重检查模式,这段代码的优点体现在哪里?
内部采用互斥锁,代码无论如何是可靠的
new出第一个实例后,后面每个线程访问到最外面的if判断就直接返回了,没有加锁的开销
我再次运行测试,(测试代码不变),结果如下:
- cost : 438 ms
啊哈,十几倍的性能差距,可见我们的改进是有效的,仅仅三行代码,却带来了十几倍的效率提升!
尾声
上面这种编写方式成为DCLP(Double-Check-Locking-Pattern)模式,这种方式一度被认为是绝对正确的,但是后来有人指出这种方式在某些情况下也会乱序执行,可以参考Scott的C++ and the Perils of Double-Checked Locking - Scott Meyer
C++Singleton的DCLP(双重锁)实现以及性能测评的更多相关文章
- volatile双重锁实现单例
双重锁实现单例时遭到质疑,既是:双重锁也无法保证单例模式! 原因是:指令会重排序,普通的变量仅仅会保证该方法在执行时,所有依赖的赋值结果是正确的,但不会保证执行顺序! 为什么会重排序:指令重排序是指c ...
- 单例模式-DCL双重锁检查实现及原理刨析
以我的经验为例(如有不对欢迎指正),在生产过程中,经常会遇到下面两种情况: 1.封装的某个类不包含具有具体业务含义的类成员变量,是对业务动作的封装,如MVC中的各层(HTTPRequest对象以Thr ...
- 单例模式的双重锁为什么要加volatile(转)
单例模式如下: 需要volatile关键字的原因是,在并发情况下,如果没有volatile关键字,在第5行会出现问题. instance = new TestInstance();可以分解为3行伪代码 ...
- 单例模式(Singleton)的同步锁synchronized
单例模式,有“懒汉式”和“饿汉式”两种. 懒汉式 单例类的实例在第一次被引用时候才被初始化. public class Singleton { private static Singleton ins ...
- Qt中实现单例模式(SingleTon),大约有3种办法
Qt中实现单例模式(SingleTon) 单例模式分为“饥汉”和“饿汉”两种版本,也正是线程安全问题使得原本简单的单例模式变得复杂.由于单例模式很常用,Boost库中有强大的泛型单例实现,我也利用Qt ...
- java常见设计模式
工厂模式 普通工厂模式,就是建立一个工厂类,对实现了同一接口的一些类进行实例的创建. 多个工厂模式,编写多个创建工厂的方法即可. 静态工厂模式,在多个工厂模式的基础上把Factory种方法的返回值标明 ...
- 单例模式双重检验锁的判断是否为null的意义
关于双重检验锁首先简单来看一个小例子: public class Singleton{ private static Singleton instance = null; private Single ...
- 双重校验锁 --使用volatile和两次判空校验
介绍 双重校验锁是单例模式中,饿汉式的一种实现方式.因为有两次判空校验,所以叫双重校验锁,一次是在同步代码块外,一次是在同步代码块内. 为什么在同步代码块内还要再检验一次? 第一个if减少性能开销,第 ...
- Singleton(单例模式)
一. /** * lazy man(不是线程安全的) * @author TMAC-J * */ public class Singleton { private static Singleton i ...
随机推荐
- Hibernate中映射一对一关联(按主键映射和外键映射)和组件映射
Hibernate中映射一对一关联(按主键映射和外键映射)和组件映射 Hibernate提供了两 ...
- 使用 padding-bottom 设置高度基于宽度的自适应
我们在做移动端列表,通常会做到图文列表,列表是自适应的.当列表中有图片,图片的宽度是随着列表宽的变化而变化,我们为了在图片宽度变化的时候做到图片的不变形,所有采用以下办法. 本文章只讲语法 html ...
- STM in Haskell
Software Transactional Memory,软件事务内存管理(应该是这么翻译的吧T_T) 类似于数据库的事务,所有的操作都有log,最后验证其他线程是否对数据进行修改,要是有那么就回滚 ...
- xCode中去除“Implicit declaration of function 'sysctl' is invalid in C99” 警告
http://blog.csdn.net/dreambegin/article/details/8609121 一般出现该问题是因为通过C调用了unix/linux 底层接口,所以需要调整c语言的编译 ...
- screenshoter 連續截圖工具
https://pcrookie.com/?p=993 顯示 mouse 設定 Settings -> Saving -> Display mouse cursor
- hdu 5166(水题)
Missing number Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 65536/65536 K (Java/Others)To ...
- 计蒜客 28449.算个欧拉函数给大家助助兴-大数的因子个数 (HDU5649.DZY Loves Sorting) ( ACM训练联盟周赛 G)
ACM训练联盟周赛 这一场有几个数据结构的题,但是自己太菜,不会树套树,带插入的区间第K小-替罪羊套函数式线段树, 先立个flag,BZOJ3065: 带插入区间K小值 计蒜客 Zeratul与Xor ...
- HDU 5732 Subway(2016多校1J,树的重心 + 哈希)
题目链接 2016多校1 Problem J 题意 给定两棵相同的树,但是编号方案不同.求第一棵树上的每个点对应的第二棵树上的点.输出一种方案即可. 首先确定树的直径的中点.两棵树相等意味着两棵树 ...
- 前端中 width 的获取
这篇文章其实是在了解 viewport 的过程中发现这些概念容易混淆做了个小小的总结.viewport的首要关键是宽度的获取,宽度的计算有下面几个属性和方法: clientWidth offsetWi ...
- linux之vim命令
:tabe fn 在一个新的标签页中编辑文件fngt 切换到下一个标签页gT 切换到上一个标签页:tabr 切换到第一个标签页:tabl 切换到最后一个标签页: ...