C++ 单例模式和双检锁问题
最近在看《程序员的自我修养》这本书,从代码的编译到链接,从虚拟空间映射物理空间,到内存的分配无一不通通展开。以前对编译,链接不了解、疑惑的地方在看这本书时都有一种豁然开朗的感觉,特此记录一下。
在设计模式中,单例模式算的上最容易理解简单,且经常用到的一种模式。单例模式又分为“饿汉式”和“懒汉式”两种模式。
- 懒汉式:需要类的实例化时去判断唯一实例是否被实例化,如果没有才会去创建实例
- 饿汉式:在类定义的时候,唯一实例就已经进行实例化,后面需要用到时,直接返回唯一实例
懒汉式
1 | //单线程安全 |
上面代码,是单线程安全的,但并不是多线程安全。考虑一下有两个线程A和B,同时调用了GetInstance方法又恰巧检测到pInst为NULL,这时就出问题了,产生了两个实例,这并不是我们想要的。接下来,我们肯定会想到线程不安全加锁就完了嘛。
双检锁-懒汉式
下面是经典的加锁懒汉式实现1
2
3
4
5
6
7
8
9
10
11T *GetInstance()
{
//进行double-check,降低多线程每次调用lock带来的开销
if (pInst == NULL) {
lock();
if (pInst == NULL)
pInst = new T;
unlock();
}
return pInst;
}
这里的双重if检测(double-check),是为了降低多线程每次调用lock带来的开销。也许当我们看到这样的代码时,认为并没有问题,实际上是有问题的,问题来自CPU的乱序执行。
我们知道C++的new,包含了两个步骤
- 分配内存
- 调用构造函数
所以pInst=new T包含了三个步骤
- 分配内存
- 调用构造函数
- 将分配好的内存地址赋值给pInst
事实上在cpu执行的时候,步骤2和3是可以颠倒的,他们看上去像这样
- 调用operator new()分配内存
- 使pInst指向分配好的内存
- 调用构造函数constructor
那在多线程的情况下,就可能会出现A线程刚好分配好内存,并赋值给pInst,B线程再次调用GetInstance方法,此时pInst已经不为空了,所以就会出现将一个还并没有构造完毕的对象直接返回给用户使用,此时问题就出现了
解决方法,使用barrier指令
1 | #define barrier() __asm__ volatile ("lwsync") |
通常情况下是调用cpu提供的一条指令,这条指令的作用是会阻止cpu将该指令之前的指令交换到该指令之后,这条指令也通常被叫做barrier。 上面代码中的asm表示这个是一条汇编指令,volatile是可选的,如果用了它,则表示向编译器声明不允许对该汇编指令进行优化。lwsync是POWERPC提供的barrier指令。
最后,在C++11中,关于C++双检锁的问题,已经完全解决了,有兴趣的朋友可以去看下C++11 DCLP