问题
在设计模式中,有一个很经典的模式-单例模式,它可能是实现上最简单的模式,在代码中也经常使用,在单线程下,毫无疑问延迟化加载是比较常用的,但是在多线程条件下,单例模式的延迟加载可能就会出现一些问题。
如以下的代码:
T* GetInstance()
{if (pInst == NULL){pInst = new T;}return pInst;
}
如果检测代码和实例化代码不是同一线程,则很容易出现返回NULL的现象。
经典的单例模式下的双重检测
解决以上问题就是加并发锁,我们将需要实例化的对象加锁,于是有了以下代码:
T* GetInstance()
{if (pInst == NULL){lock();if (pInst == NULL)pInst = new T;unlock();}return pInst;
}
为什么要用两层if检查,第一层的if检查是因为当实例为空的时候,才去对实例加锁,这样可以避免多次对lock资源的调用,当第二层if检测的时候,才是程序要对程序进行初始化。
乍看这种代码是没有问题的,但是问题的来源是CPU的乱序执行,C++的New操作实际上包含了两个步骤:
- 分配内存
- 调用构造函数
所以pInst = new T包含了三个步骤:
- 分配内存
- 在内存的位置上调用构造函数
- 将内存的地址赋值给pInst
因为(2)和(3)是可以颠倒的,所以可以出现这样的情况:pInst的值已经不是NULL,但对象仍然没有构造完毕。如果另外一个线程对GetInstance的调用,此时第一个if为false,这样就会返回一个未构造完成的对象,此时可能会导致程序崩溃。
解决思路
许多体系结构都提供barrier指令,POWERPC提供了其中一条名为lwsync的指令,我们可以这样来保证线程安全:
#define barrier() __asm__ volatile ("lwsyc")
volatile T* pInst = 0;
T* GetInstance()
{if (!pInst) {lock();if (!pInst){T* temp = new T;barrier()pInst = temp;}unlock();}return pInst;
}
由于barrier的存在,对象的构造一定会在barrier执行之前完成,所以这样不会出现一些问题。