之前去一个公司面试的时候被问到了Singleton模式,本以为Singleton是“最简单”的一个模式,于是便按照Gamma等人的《设计模式》一书写了个C++实现。结果被面试官当即指出这个实现不是现成安全的,好吧,原来是踩入陷阱里了。于是上网搜索,发现此设计模式还是很有讲究的。
实现0:全局变量和静态变量
有人会问,为啥要用这个设计模式呢?为啥不直接定义一个全局或静态变量呢,不就能保证只有一个实例的吗?对于这个问题,《设计模式》书中就有答案,书中给出了三个理由。
1. 无法保证,只有一个变量被定义。很有可能在不同的实现文件里定义了多个变量,这样就不是一个实例了。我的理解是,使用这种方式,实例的个数必须由开发人员自己掌握,如果哪个不长眼的, 比如分不清声明和定义的区别的,在维护代码的时候,一不小心多定义了一个实例,这样整个系统就会出问题了。
2. 可能没有足够的信息来初始化一个实例。如果有些类的初始化数据是在系统开始运行后才能得到的,显然无法在系统初始化的时候来初始化Singleton的实例了。
3.多个Singleton之间不能有依赖关系。这一点其实本人还不是很理解。大概的意思是如果有多个全局Singleton,而且他们互相有依赖关系,由于C++在这种情况下没有定义其初始化顺序,所以会引入错误。
4. 显而易见的就是,不管用没有,都生成了一个实例。
由于以上原因,我们不能简单的用静态或全局变量来是些Singleton的要求。
这种情况下有以下两种实现:
一种实现其实就是在类中定义一个该类的静态成员。
class Singleton
{
private:
staitc Singleton _instance;
public:
static Singleton &getInstance()
{
return _instance;
}
};
另一种实现是使用静态方法的静态变量。
class Singleton
{
public:
static Singleton &getInstance()
{
staitc Singleton instance;
return instance;
}
};
这种实现和第一中相比略有改进。实例只在getInstance被调用时初始化。但是在一般情况下它并非线程安全的,这和具体的C语言实现相关。
实现1: 典型实现
《设计模式》书中描述的实现是这样的。
class Singleton
{
private:
staitc Singleton *_instance;
public:
static Singleton &getInstance()
{
if(_instance == NULL)
_instance = new Singleton();
return _instance;
}
};
Singleton * Singleton::_instance = NULL;
这也就是我在面试时写的代码,而这个代码的一个严重问题就是该代码不是线程安全的。如果多个线程同时进入了那个if判断,则他们会得到不同的实例。
实现2: 加锁
所以很显然的改进就是加锁:
Singleton* Singleton::instance()
{
Lock lock; // acquire lock (params omitted for simplicity)
if (_instance == NULL)
{
_instance = new Singleton;
}
return _instance;
} // release lock (via Lock destructor)
但是这样写对性能的影响又太大,由于竞争只在if里面发生,于是便会想要该进一下。
Singleton* Singleton::instance()
{
if (_instance == NULL)
{
Lock lock; // acquire lock (params omitted for simplicity)
_instance = new Singleton;
}
return _instance;
} // release lock (via Lock destructor)
然而这样是错误的,因为两个线程可能同时进入if,虽然在lock处会互斥,但是依然创建了两个实例。所以正确的做法是两次检查。
Singleton* Singleton::instance()
{
if (_instance == NULL)
{
Lock lock; // acquire lock (params omitted for simplicity)
if(_instance == NULL)
_instance = new Singleton;
}
return _instance;
} // release lock (via Lock destructor)
第一次检查是为了提高性能,而第二次是为了确保不会创建两个实例。但是这个实现还有一个问题:
就是编译器优化后可能会变成如下顺序:
1. 分配地址给 _instance
2. 初始化Singleton
而一般的理解应该是先初始化Singleton再把地址赋予_instance。优化后的代码就会出现如下问题。当把一个地址分配给_instance却没有初始化Singleton时另一个线程可能返回_instance,而这个instance其实并没有被初始化,这样悲剧就发生了。
如何解决呢?请参考: http://c.chinaitlab.com/skill/798679_5.html
http://en.wikipedia.org/wiki/Singleton_pattern#C.2B.2B