当代码的临界区必须在程序执行期内以线程安全的方式获得一次锁时,双检查加锁优化设计模式能减少争用和同步开销。
例子:
class Singleton
{
public:
static Singleton *instance()
{
if(m_instance == NULL)
{
m_instance = new Singleton();
}
return m_instance;
}
void SayHello()
{
std::cout<<"Hello"<<std::endl;
}
private:
static Singleton *m_instance;
};
Singleton* Singleton::m_instance = NULL;
上述单例模式在具有抢先多任务或真正的硬件并行性平台上是有问题的。如多个抢先线程在初始化之前同事调用Singleton::instance(); 为了使用临界区不被并发访问,可以使用定界加锁自动得获取和释放一个互斥锁。
class Singleton
{
public:
static Singleton *instance()
{
ScopedLock lock( _mutex() );
if(m_instance == NULL)
{
m_instance = new Singleton();
}
return m_instance;
}
void SayHello()
{
std::cout<<"Hello"<<std::endl;
}
private:
static Mutex & _mutex()
{
static Mutex m;
return m;
}
static Singleton *m_instance;
};
现在Singleton是线程安全的。不过加锁开销会很大。每次对instance()的调用都有获取和释放锁。可以将哨兵放在条件检查里面,从而避免加锁开销。
static Singleton *instance()
{
if(m_instance == NULL)
{
ScopedLock lock( _mutex() );
m_instance = new Singleton();
}
return m_instance;
}
可以,这种解决方案不能提供线程安全的初始化,因为多线程应用中竞争条件可以使用Singleton多次被初始化。例如:考虑两个同时检查m_instance == NULL的线程,假设两个都成功,一个通过guard获取锁然后释放锁,而另一个将阻塞。在第一个线程初始化Singleton并释放锁后,那个被阻塞的线程会得到锁,并错误的再次初始化Singleton。
解决方案:使用双检查加锁模式
static Singleton *instance()
{
if(m_instance == NULL)
{
ScopedLock lock( _mutex() );
if(m_instance == NULL)
{
m_instance = new Singleton();
}
}
return m_instance;
}
总结
优点:
1.使加锁开销最小。
2.防止竞争条件。
缺点:
1.非原子指针复制语义。如:new之后写内存操作不是原子的,其他线程可能读到一个无效的指针。如将32位指针用于16位总线的计算机上,需要访问两次内存。
2.多处理器缓存的连贯性。一些多处理器平台进行积极的内存缓存优化,可以在多cpu缓存间无序的执行读写操作,必须实现中插入与CPU有关的指令,如清除缓存线的内存障栏。
3.额外的互斥使用。