Singleton是我使用过的最多的设计模式,也是日常工作中大家会经常用到的设计模式。其实,在C++里面写一个Singleton,不是一件非常容易的事情,以至于《C++设计新思维》里面花了一章内容专门讲解。难点在哪里呢?其实就是两个:
(1)多线程的并发性 (如果你的程序是单线程的,那么就没有这个问题)
(2)生命周期
下面我们就开始这个旅程。另外一点,我现在使用C++的原则是KISS,保持设计和代码的简单性。
首先,我们先来看看这两个问题。
(1)多线程的并发性,这不难理解。如果在单线程模式下,我们通常会这么写Singleton:
// Singleton.h
class Singleton
{
public:
~Singleton(){}
static Singleton& getInstance();
private:
Singleton(){}
};
// Singleton.cpp
/* static */
Singleton& Singleton::getInstance()
{
static Singleton *instance = new Singleton;
return &instance;
}
这种方法是按照《Effective C++》里面的建议,不使用class static变量,而使用函数static变量。这个是延迟初始化,如果整个过程没有函数调用Singleton::getInstance(),那么就不会有Singleton这个对象生成。为什么返回一个Reference,而不是一个指针,是因为这样不容易被误delete。可是如果是多线程呢?有Java经验的同学都会想到下面这个方法:
// Singleton.h 错误的实例
class Singleton
{
public:
~Singleton(){}
static Singleton& getInstance();
private:
Singleton(){}
static Lock lock_;
};
// Singleton.cpp
/* static */
Lock Singleton::lock_;
Singleton& Singleton::getInstance()
{
static Singleton *instance = NULL;
if (NULL == instance) // thread B Pos1
{
LockHandler handler(lock_);
if (NULL == instance)
{
instance = new Singleton; // thread A Pos2
}
}
return &instance;
}
(注解:Lock是类似与封装了pthread_mutex_t的类,具有lock(), unlock(), trylock()这样的方法;而LockHandler的构造函数就会调用lock_.lock(),析构函数会调用lock_.unlock(),这是一种RIIA的惯用法)。这段代码非常Java的Singleton代码,它有一个响亮的名字:两次检查。
public class Singleton {
private Singleton(){}
private static Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized(Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
}
}
上面这段Java代码在Java5之后是正确的,在Java5之前,在有些平台上这段代码是有bug的。那C++的两次检查呢?是错误的。知道为什么这段代码是错误的,只会让让你很郁闷,因为instance = new Singleton这句话,根据正常的理解,应该是Singleton分好内存,然后再调用构造函数,然后再把那个地址赋值给instance。可是事实上这是不一定的,有可能先完成这次赋值,再调用构造函数。也许你应该明白为什么有问题了吧。假设threadA到了pos2那个地方,此时threadB刚好来到pos1,然后它发现instance==null不成立,于是threadB就可以使用instance了。可是此时,instance指向的对象还没有正常的调用构造函数。换句话说,threadB使用了一个没有被正确初始化的对象。看到这里也许你要哭了,没错,我也哭了。这不是我们程序员本需要考虑的问题,可是它就是像座山一样在那里。没办法,如果你使用C++,那么请接受它吧。我可以说这种问题很难测试到,也许它会跟随你的系统几个月甚至上年,可是有一天它突然崩溃了,你才知道原来是这样造成的。没办法,那我们退回到一个保守的地方:
// Singleton.h
class Singleton
{
public:
~Singleton(){}
static Singleton& getInstance();
private:
Singleton(){}
static Lock lock_;
};
// Singleton.cpp
/* static */
Lock Singleton::lock_;
Singleton& Singleton::getInstance()
{
static Singleton *instance = NULL;
LockHandler handler(lock_);
if (NULL == instance) // thread B Pos1
{
instance = new Singleton; // thread A Pos2
}
return &instance;
}
这个代码是对的,正确的。可是它有些效率问题,因为你每次访问这个资源的时候,都需要去竞争这个lock_。我们需要继续优化这个。(未完待续)