本文系原创,转载请注明: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(双重锁)实现以及性能测评的更多相关文章

  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. 狂K 线段树

    想写好树剖~~线段树very important HDU 1166 敌兵布阵 Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 65536 ...

  2. ASIHttprequest-创建同步请求

    ASIHttprequest-创建同步请求 当你发送一个同步请求后,该请求会在当前的应用主线程中运行并获取程序的控制权限,也就说你的程序处于锁定的状态,在这个期间,你进行不了任何的操作,直到该请求返回 ...

  3. poj 3744 Scout YYF 1 (概率DP+矩阵快速幂)

    F - Scout YYF I Time Limit:1000MS     Memory Limit:65536KB     64bit IO Format:%I64d & %I64u Sub ...

  4. 求LCA最近公共祖先的在线ST算法_C++

    ST算法是求最近公共祖先的一种 在线 算法,基于RMQ算法,本代码用双链树存树 预处理的时间复杂度是 O(nlog2n)   查询时间是 O(1) 的 另附上离线算法 Tarjan 的链接: http ...

  5. error LNK2001: 无法解析的外部符号 "public: virtual long __stdcall CBaseFilter(转)

    原文转自 https://www.cnblogs.com/xiongjiaji/archive/2010/12/31/2476565.html 今天用VS2005编译DirectShow程序,发现出来 ...

  6. fmap为什么可以用function作为第二个参数

    看看fmap的类型 fmap :: Functor f => (a -> b) -> f a -> f b 很明显的,第一个参数是function,第二个参数是functor的 ...

  7. 将本地访问ip映射成域名

    通过修改以下地址 C:\WINDOWS\system32\drivers\etc\hosts 加进你自己的如: 192.168.1.101  www.helloworld.com 配置nginx代理u ...

  8. Django和SQLAlchemy区别

    译者注:本文首先介绍了什么是ORM,然后从多个方面对Python语言下的两个ORM库Django和SQLAlchemy进行比较,为ORM的选型提供了较为全面的指导建议.以下是译文. ORM是什么? 在 ...

  9. [BZOJ1634][Usaco2007 Jan]Protecting the Flowers 护花 贪心

    1634: [Usaco2007 Jan]Protecting the Flowers 护花 Time Limit: 5 Sec  Memory Limit: 64 MBSubmit: 885  So ...

  10. DB2如何调整表空间大小

    DB2如何调整表空间大小 刚刚接到客户那边打的电话,程序一直报错,所有的业务都做不了,拷贝了一份应用服务器(weblogic)的日志,日志里显示: WARN : 2009-06-18 16:24:32 ...