线程安全的单例模型的演变与Double-Check-Locking的安全性

本文探讨了线程安全单例模式的实现,从最初的简单实现到双检查锁定(DCL)模式,分析了DCL存在的问题,主要是由于CPU乱序执行和编译器优化。提出了volatile关键字、内存屏障等解决方案,并强调在编写线程安全的单例时,应避免过度依赖DCL,可以采用缓存单例等方式减少锁竞争。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

拜读了论文《C++ and the Perils of Double-Checked Locking》-- Scott Meyers and Andrei Alexandrescu September 2004
发现原来一个简单的单例模型,还有如此多的文章,上面提到的这篇文章值得反复读,从中可以衍生出很多并行编程的知识。
另外,C++11的内存模型和std::memory以及原子类,是我接下来关心的重点。

姑且把这个文章叫做《线程安全的单例模型演变》吧,从最简单的讲起

1、第一种实现,很简单
class Singleton {
 public:
   static Singleton* instance();
 private :
   static Singleton* pInstance;
};
Singleton* Singleton::instance() {
  if (pInstance == 0) { //(*)
    pInstance = new Singleton;
  }
  return pInstance;
}
这种单例模式的实现,很简单,但是是存在问题的。在(*)行我们可以看到,这个 ==0 的的检测,是没有多线程互斥的。所以,一旦有多个线程同时检查 ==0 条件为true,那么每个线程都会去 new Singleton。这样结果就不对了,而且会导致内存泄露。

2、第二种实现,加锁
Singleton* Singleton::instance() {
  Lock lock; // acquire lock (params omitted for simplicity)
  if (pInstance == 0) { //(*)
    pInstance = new Singleton;
  }
  return pInstance;
} // release lock (via Lock destructor)
因为第一种实现方式错误的根源在于,==0的检测没有做到多线程的互斥,所以,我们可以在新的实现方式中,我们在检测 ==0 之前加锁了。这是一种解决问题很好的方法,但是每次获取这个单例对象的时候,都要加锁,性能会严重受到影响。在程序运行的过程中,除了第一次new发生的时候,(*)行这个==0的检测结果都是false的,这个加锁,有点浪费。
看下来,这种实现存在的是性能上的问题。那我们往后面看。

3、好了,我们大名鼎鼎的 Double-Checked-Locking来了,看代码:
Singleton* Singleton::instance() {
  if (pInstance == 0) { // 1st test
  Lock lock;
    if (pInstance == 0) { // 2nd test
      pInstance = new Singleton;
    }
  }
  return pInstance;
}
这是一种很经典的实现,因为我们对指向单例对象的指针,写了两次的==0检测。因为程序运行过程中,大部分的时候,这个指针都是 !=0的,所以这个锁就可以避免了,只有在第一次new Singleton之前,可能会发生锁竞争。一旦new完成之后,锁就不会被用到了。

愿景是美好的,但是现实是残酷的,上面的实现仍然存在问题。问题源自于CPU的乱序执行。
简单讲一下乱序执行的概念,在现代CPU的实现中,允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理。注意关键字:“不安程序规定的顺序”。
对于我们上面代码中一个语句: pInstance = new Singleton; 在计算机里面会被分解为3个指令:
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值