Fork me on GitHub

C++ 单例模式 双检锁问题

C++ 单例模式和双检锁问题

最近在看《程序员的自我修养》这本书,从代码的编译到链接,从虚拟空间映射物理空间,到内存的分配无一不通通展开。以前对编译,链接不了解、疑惑的地方在看这本书时都有一种豁然开朗的感觉,特此记录一下。

 在设计模式中,单例模式算的上最容易理解简单,且经常用到的一种模式。单例模式又分为“饿汉式”和“懒汉式”两种模式。

  • 懒汉式:需要类的实例化时去判断唯一实例是否被实例化,如果没有才会去创建实例
  • 饿汉式:在类定义的时候,唯一实例就已经进行实例化,后面需要用到时,直接返回唯一实例

懒汉式

1
2
3
4
5
6
7
//单线程安全
T *GetInstance()
{
if (pInst == NULL)
pInst = new T;
return pInst;
}

上面代码,是单线程安全的,但并不是多线程安全。考虑一下有两个线程A和B,同时调用了GetInstance方法又恰巧检测到pInst为NULL,这时就出问题了,产生了两个实例,这并不是我们想要的。接下来,我们肯定会想到线程不安全加锁就完了嘛。

双检锁-懒汉式

下面是经典的加锁懒汉式实现

1
2
3
4
5
6
7
8
9
10
11
T *GetInstance()
{
//进行double-check,降低多线程每次调用lock带来的开销
if (pInst == NULL) {
lock();
if (pInst == NULL)
pInst = new T;
unlock();
}
return pInst;
}

 这里的双重if检测(double-check),是为了降低多线程每次调用lock带来的开销。也许当我们看到这样的代码时,认为并没有问题,实际上是有问题的,问题来自CPU的乱序执行。
 我们知道C++的new,包含了两个步骤

  1. 分配内存
  2. 调用构造函数

 所以pInst=new T包含了三个步骤

  1. 分配内存
  2. 调用构造函数
  3. 将分配好的内存地址赋值给pInst

事实上在cpu执行的时候,步骤2和3是可以颠倒的,他们看上去像这样

  1. 调用operator new()分配内存
  2. 使pInst指向分配好的内存
  3. 调用构造函数constructor

那在多线程的情况下,就可能会出现A线程刚好分配好内存,并赋值给pInst,B线程再次调用GetInstance方法,此时pInst已经不为空了,所以就会出现将一个还并没有构造完毕的对象直接返回给用户使用,此时问题就出现了

解决方法,使用barrier指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#define barrier() __asm__ volatile ("lwsync")
T *GetInstance()
{
if (pInst == NULL) {
lock();
if (pInst == NULL) {
T *temp = new T;
barrier();
pInst = temp;
}
unlock();
}
return pInst;
}

 通常情况下是调用cpu提供的一条指令,这条指令的作用是会阻止cpu将该指令之前的指令交换到该指令之后,这条指令也通常被叫做barrier。 上面代码中的asm表示这个是一条汇编指令,volatile是可选的,如果用了它,则表示向编译器声明不允许对该汇编指令进行优化。lwsync是POWERPC提供的barrier指令。
 最后,在C++11中,关于C++双检锁的问题,已经完全解决了,有兴趣的朋友可以去看下C++11 DCLP

您的赞赏是对我最大的支持,谢谢!