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

时间: 2024-10-01 17:45:23

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

C++——单例模式的DCLP(双重锁)实现以及性能测评

单例模式的描述是: 确保一个类只有一个实例,并提供对该实例的全局访问. 从这段话,我们可以知道,单例模式的最重要特点就是:一个类最多只有一个对象. 对于一个普通类,我么可以生成任意对象,我们为了避免生成太多的类,需要将类的构造函数设为私有. 这样的话,我们为了获取实例,只能借助于类的内部函数,而且必须是static函数(非static函数中均包含一个隐式参数this,由于我们没办法实例化,所以只能通过static函数来获取实例): 1 class Singleton 2 { 3 public:

单例模式的简单描述(饿汉式,饱汉式,双重锁模式)

一.什么时候使用单例模式: 当实例存在多个会引起程序逻辑错误的时候 二.好处: 1.减少内存的占用 2.单例模式会阻止其他对象实例化其自己的单例对象的副本,从而确保所有对象都访问唯一实例. 3.因为类控制了实例化过程,所以类可以灵活更改实例化过程 三.缺点: 1.开销 虽然数量很少,但如果每次对象请求引用时都要检查是否存在类的实例,将仍然需要一些开销.可以通过使用静态初始化解决此问题. 2.可能的开发混淆 使用单例对象(尤其在类库中定义的对象)时,开发人员必须记住自己不能使用new关键字实例化对

设计模式一:饱汉式单例(双重锁)

一.简介 单例的目的保证该对象只能存在一个,只有应用场景相当多,列如jedisUtils.. 二.代码 测试代码如下: /** * @ClassName: SingeTest * @Description: 饱汉式单例 * @author: ck * @date: 2019年2月23日 下午3:37:47 */ public class SingeTest { public static SingeTest singeTest=null; // 将构造方法私有化,使其只能通过getSingeTe

无锁编程与有锁编程的性能对比与分析

最近维护的一个网络服务器遇到性能问题,于是就对原有的程序进行了较大的框架改动.改动最多的是线程工作模式与数据传递方式,最终的结果是改变锁的使用模式.经过一番改进,基本上可以做到 GMb 网卡全速工作处理.在 性能达标之后,一度在想有没有什么办法使用更加轻量级锁,或者去掉锁的使用,为此搜索一些相关的研究成果,并做了一些实验来验证这些成果,因而就有这篇文章.希望有做类似工作的同行可以有所借鉴.如果有人也有相关的经验,欢迎和我交流. 1 无锁编程概述 本节主要对文献 [1] 进行概括,做一些基础知识的

双重锁学习 —— Double-checked locking: Clever, but broken Do you know what synchronized really means?

From the highly regarded Elements of Java Style to the pages of JavaWorld (see Java Tip 67), many well-meaning Java gurus encourage the use of the double-checked locking (DCL) idiom. There's only one problem with it -- this clever-seeming idiom may n

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

单例模式,有"懒汉式"和"饿汉式"两种. 懒汉式 单例类的实例在第一次被引用时候才被初始化. public class Singleton { private static Singleton instance=null; private Singleton() { } public static Singleton getInstance(){ if (instance == null) { instance = new Singleton(); } return

悲观锁下的性能优化思路

一般使用悲观锁(for update),都是防止并发情况下对某个业务表数据同时进行修改操作. 这个时候该如何去优化呢? 一般悲观锁,如果按照索引去查询,锁住的是行级锁.一般是单个账户一行记录. 这个时候可以考虑将要批量的更改业务数据,按照用户分组,然后开启多线程去处理. 这样能让多个用户之间同时处理,而不需要进行等待. ps:使用悲观锁要注意尽量别产生间隙锁,否则非常影响性能. 原文地址:https://www.cnblogs.com/jylsgup/p/10657818.html

互斥锁、自旋锁、dispatch_once性能对比

代码: #import <Foundation/Foundation.h> #import <pthread.h> extern uint64_t dispatch_benchmark(size_t count, void (^block)(void)); // pthread_mutex_lock void dispatch_once_pthread(dispatch_once_t *, dispatch_block_t); // spinlock void dispatch_o

问题整理(常问3连)

1.static final关键字的作用 static final用来修饰成员变量和成员方法,可以理解为“全局变量” final成员变量表示常量,只能被赋值一次,赋值后不能再被改变 final类不能被继承,final类中的方法默认是final的 final方法不能被子类的方法复盖 static表示“静态”的意思 用来修饰成员变量和成员方法 静态变量:它不属于任何实例对象,是属于类的,所以在内存中只会有一份,在类的加载过程中,JVM为静态变量分配一次内存空间 2.bio.nio.aio 区别 BI