c++中的 单例模式(singleton)和双检测锁(Double-Checked Locking)

今天学习了一下c++中的singleton。google了一篇论文C++ and the Perils of Double-Checked Locking。大名鼎鼎的Scott Meyers写的。论文使用c++讲解,看了之后受益匪浅。

巧的是,读完之后刚好看见http://coolshell.cn酷壳站长陈皓大哥的一篇文章http://blog.csdn.net/haoel/article/details/4028232也是讲的这个问题。不同于上面那篇文论,陈皓大哥用的是java讲解。

我想做的是,还原一下那篇论文,算是一个学习总结吧。也是对陈皓大哥那篇文章的一个补充吧。

你去google一下设计模式这四个字,肯定有提到单例(singleton)这个经典设计模式。but but but,,,,,传统的单例模式实现不是线程安全的!!!

为了解决线程安全问题,程序员们做了很多努力,目前最流行的就是double-checked locking pattern(dclp). dclp可为共享资源(比如singleton)添加有效的线程安全。

but but but DCLP也有不足之处,比如不可复用,不能方便地移植,并且在多核处理器系统中也起不到作用。今儿不说这些不足,只说说DCLP怎么解决线程安全问题的。

 

单例和多线程

 

传统的单例模式实现:

 

class Singleton
{
private:
    Singleton(){}
public:
    static Singleton* instance()
    {
        if(_instance == 0)
        {
            _instance = new Singleton();
        }

        return _instance;
    }
private:
    static Singleton* _instance;

public:
    int atestvalue;
};

Singleton* Singleton::_instance = 0;


上面这种实现在单线程环境下是没有问题的,可是多线程下就有问题了。

 

稍微分析一下:

1. 例如线程A进入函数instance执行判断语句,这句执行后就挂起了,这时线程A已经认为_instance为NULL,但是线程A还没有创建singleton对象。

2. 又有一个线程B进入函数instance执行判断语句,此时同样认为_instance变量为null,因为A没有创建singleton对象。线程B继续执行,创建了一个singleton对象。

3. 稍后,线程A接着执行,也创建了一个新的singleton对象。

4. fuck!!两个对象!

从上面分析可以看出,需要对_instance变量加上互斥锁:

 

Singleton* Singleton::instance() {
    Lock lock; // acquire lock (params omitted for simplicity)
    if (_instance == 0) {
        _instance = new Singleton;
    }
    return _instance;
} // release lock (via Lock destructor)


上锁后是解决了线程安全问题,但是有些资源浪费。稍微分析一下:每次instance函数调用时候都需要请求加锁,其实并不需要,instance函数只需第一次调用的时候上锁就行了。这时可以用DCLP解决。

 

 

Double-Checked Locking Pattern

 

Singleton* Singleton::instance() {
    if (_instance == 0) { // 1st test
        Lock lock;
        if (_instance == 0) { // 2nd test
            _instance = new Singleton;
        }
    }
    return _instance;
}


DCLP and Instruction Ordering

 

 

我们来仔细打量一下这句代码:

 

 

 

_instance  = new singleton()

为了执行这句代码,机器需要做三样事儿:

 

1.singleton对象分配空间。

2.在分配的空间中构造对象

3.使_instance指向分配的空间

遗憾的是编译器并不是严格按照上面的顺序来执行的。可以交换2和3.

将上面三个步骤标记到代码中就是这样:

 

Singleton* Singleton::instance() {
    if (_instance == 0) {
        Lock lock;
        if (_instance == 0) {
        _instance = // Step 3
        operator new(sizeof(Singleton)); // Step 1
        new (_instance) Singleton; // Step 2
        }
    }
    return _instance;
}


好了,紧张的时刻到了,如果发生下面两件事:

 

  • 线程A进入了instance函数,并且执行了step1和step3,然后挂起。这时的状态是:_instance不NULL,而_instance指向的内存去没有对象!
  • 线程B进入了instance函数,发现_instance不为null,就直接return _instance了。

貌似这时无法解决的问题了,咋办呢。搞嵌入式的程序员可能想到用c++中的volatile关键字。对,就是用volatile,但是用volatile就要一用到底,用了之后就是下面这种丑陋的代码了。

 

class Singleton {
public:
    static volatile Singleton* volatile instance();
...
private:
// one more volatile added
    static Singleton* volatile _instance;
};

// from the implementation file
volatile Singleton* volatile Singleton::_instance = 0;
volatile Singleton* volatile Singleton::instance() {
    if (_instance == 0) {
    Lock lock;
        if (_instance == 0) {
        // one more volatile added


        Singleton* volatile temp = new Singleton;
        _instance = temp;
        }
    }
    return _instance;
}


其实上面完全使用volatile关键字的代码也不能保证正常工作在多线程环境中。具体原因分析请参考C++ and the Perils of Double-Checked Locking这篇论文,文章也给出了终极解决方法。

 

 

  • 6
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值