参考:http://blog.sina.com.cn/s/blog_597a437101011o66.html 最后的barrier部分自己补充的。
多线程问题也常常和一种lazy-initialize的设计模式联系在一起。在这里就会慢慢引出double-check。lazy-initialize讲的是,对于一些特别复杂的对象,让程序在第一次调用它的时候再对它进行初始化,而且保证仅仅初始化一次。
首先想到的设计是这样的:
Class A
{
private ComplexClass _result = null;
public ComplexClass GetResult()
{
if(_result == null)
{
_result = new ComplexClass();
}
return _result;
}
}
但是这样有一个问题。ComplexClass的构造过程较长的话,当第一个线程还在进行ComplexClass构造的时候,_result可能是null,也可能指向了一个尚未初始化完成的对象。这样,要么两个线程初始化了两次ComplexClass,要么第二个线程会返回一个指向不完整对象的引用。所以,在这里需要用到一个锁,如下所示:
ComplexClass GetResult()
{
lock(_lock)
{
if(_result == null)
{
_result = new ComplexClass();
}
}
return _result;
}
这样,虽然多线程的问题解决了,但是每一次需要使用result时都会请求锁,而请求锁对程序的性能是有很大影响的,因此我们在lock的外面再加一层check:
ComplexClass GetResult()
{
if(_result == null)
{
lock(_lock)
{
if(_result == null)
{
_result = new ComplexClass();
}
}
}
return _result;
}
这样,对于所有初始化完成后的请求,就都不用请求锁,而是直接返回_result。也就是说两个if的作用是为了让锁的请求调用开销减小。
但是还是存在一点问题。对于一些编程语言来说,_result = new ComplexClass();这句代码是三个步骤:1:分配动态内存,2:在内存上进行构造函数的调用,3:返回内存地址给指针_result。但是步骤2和步骤3有时候顺序会颠倒。这会使得_result指向一个仅有内存但是还没有完全初始化的对象。如果此时,线程B并发访问,那么线程B会判断_result已经不是null了,而这时其实初始化尚未完成,这时线程B就直接返回了一个部分初始化的对象,会造成程序的崩溃。那么,这个问题怎么解决呢?一般的解决方法是在程序内部再加一个局部变量(标识变量)做一层缓冲:
ComplexClass GetResult()
{
ComplexClass result;
if(_result == null)
{
lock(_lock)
{
if(_result == null)
{
result = new ComplexClass();
_result = result;
}
}
}
return _result;
}
当然,CPU提供的barrier指令也有上述功能,该指令的作用是让barrier前后的指令不会互相越界,就是barrier前面的指令不会跑到后面执行,反之亦然。所以上面的代码也可以再加上一个barrier()。
ComplexClass GetResult()
{
if(_result == null)
{
lock(_lock)
{
if(_result == null)
{
ComplexClass result = new ComplexClass();
barrier();
_result = result;
}
}
}
return _result;
}
这样,上面的问题就彻底解决了~