设计模式系列之单例模式

单例模式是使用最广泛,也最简单的设计模式之一,作用是保证一个类只有一个实例。单例模式是对全局变量的一种改进,避免全局变量污染命名空间。因为以下几个原因,全局变量不能作为单例的实现方式:

1. 不能保证只有一个全局变量

2. 静态初始化时可能没有足够的信息创建对象

3. c++中全局对象的构造顺序是未定义的,如果单件之间存在依赖将可能产生错误

单例模式的实现代码很简单:

//singleton.hpp
#ifndef SINGLETON_HPP
#define SINGLETON_HPP

class Singleton{
  public:
    static Singleton* getInstance();
  private:
    static Singleton* pInstance;
};
#endif

1 //singleton.cpp
2 #include "singleton.hpp"
3
4 Singleton* Singleton:: pInstance = nullptr;
5
6 Singleton* Singleton::getInstance(){
7   if(nullptr == pInstance){
8     pInstance = new Singleton;
9   }
10  return pInstance;
11 }

单例模式这么简单,本来讲到这里就可以结束了。不过如果把上面代码放到多线程编程中使用就不那么可靠了。在《C++and the Perils of Double-Checked Locking》这篇文章中,Scott Meyers和Andrei Alexandrescu以单例模式为例详细讲述了多线程编程中的坑。下面的内容基本出自这篇论文,跟大家分享一下,非常经典。

上面的实现在单线程时没有问题,现在假设有两个线程A和B,A执行到第8行后因中断挂起,这时候instance还没有创建,B执行到第8行,于是A和B都会创建Singleton对象,

现在就有两个单例对象了,这当然是错误的。改成线程安全很不难,进入 getInstance加个锁就能保证每次只有一个线程进入函数,于是只会有一个线程实例化 pInstance。

Singleton* Singleton::getInstance(){
Lock lock;
  if(nullptr == instance){
    pInstance = new Singleton;
  }
  return pInstance;
}

但是每次调用 getInstance都加锁是一件效率非常低的事情,特别是这里只有第一次实例化 pInstance 时才需要互斥,以后都不需要锁。于是DCLP(Double-Checked Locking Pattern)产生了。

DCLP的经典实现如下:

Singleton* Singleton::instance() {
  if (pInstance == 0) {
    // 1st test
    Lock lock;
    if (pInstance == 0) {
      // 2nd test
      pInstance = new Singleton;
    }
  }
  return pInstance;
}

通过两次检测 pInstance,这样实例化后所有的调用都不需要加锁。看样子问题已经解决了,互斥锁保证了只有一个线程会实例化 pInstance,以后的调用不需要锁,性能也不会有问题,很完美是不是。让我们一步步来看看这里面隐藏的坑。

pInstance = new Singleton;

这条实例化语句其实做了3件事情:

1. 分配一块动态内存

2. 在这块内存上调用Singleton构造函数构造对象

3. pInstance指向这块内存

问题的关键是第2和第3步可能会被编译器因优化原因调换顺序,先给pInstance赋值,在构建对象。在单线程上这是行的通的,因为编译器优化的原则是不改变结果,调换2,3两步对结果并没有影响。于是代码就类似于下面这样:

Singleton* Singleton::instance() {
  if (pInstance == 0) {
    Lock lock;
    if (pInstance == 0) {
      pInstance =   // Step 3
      operator new(sizeof(Singleton)); // Step 1
      new (pInstance) Singleton; // Step 2
    }
  }
  return pInstance;
}

再来考虑两个线程A和B,

1. A第一次检查 pInstance,获取锁,执行第1和第3步,挂起,这时候 pInstance非空,但是还没有调用构造函数,pInstance指向的是未初始化内存。

2. 线程B检查 pInstance,发现非空,于是跳出函数,后面开始使用 pInstance,一个未初始化的对象。

DCLP只有在步骤1,2,3按照严格顺序执行时才能保证正确,然而,c/c++并没有这方面的支持,c/c++语言本身没有多线程,编译器优化只要保证单线程语义正确就行,多线程是不考虑的。为了保证第2步在第3步之前完成,可能需要增加一个临时变量,

Singleton* Singleton::instance() {
  if (pInstance == 0) {
    Lock lock;
    if (pInstance == 0) {
      Singleton* temp = new Singleton; // initialize to temp
      pInstance = temp;
      // assign temp to pInstance
    }
  }
  return pInstance;
}

很可惜,temp很可能也会被编译器优化掉。为了防止优化,文章围绕volatile关键字做了详细的讨论,刘未鹏以及何登成都深入解释了volatile关键字在多线程编程中的效果,volatile明确告诉编译器不要对被修饰的变量做优化,包括读写值时必须直接读取内存值,两个volatile变量的先后顺序不可变等。不过

1. volatile只能保证单线程内指令顺序不变,不能保证多线程间的指令顺序的正确性

2. 一个volatile对象只有在构造函数完成后才具有volatile特性,所以仍然存在前面讨论的问题。

总之,volatile无法保证多线程正确。

另外,在多处理器机器上,还存在cache一致性问题。如果线程A和B在不同的处理器上,

即使A严格按照1,2,3步骤执行,在将cache写回主存的过程中仍然可能改变顺序,因为按照内存地址升序顺序写回数据可以提高效率。

彻底的解决方法是使用memory barrier,这篇文章给出了c++11中的做法,

std::atomic<Singleton*> Singleton::instance;
std::mutex Singleton::m_mutex;

Singleton* Singleton::getInstance(){
  Singleton* tmp = instance.load(std::memory_order_relaxed);
  std::atomic_thread_fence(std::memory_order_acquire);
  if(nullptr == tmp){
    std::lock_guard<std::mutex> lock(m_mutex);
    tmp = instance.load(std::memory_order_relaxed);
    if(nullptr == tmp){
      tmp = new Singleton();
      std::atomic_thread_fence(std::memory_order_release);
      instance.store(tmp, std::memory_order_relaxed);
    }
  }
  return instance;
}

为了实现线程安全的DCLP,可谓费劲周章。其实有时候我们也可以采取另外的解决问题的方式,比如多线程程序开始只有主线程,我们可以先在主线程中初始化单例模式,然后再创建其他线程,从而完全避免以上问题,这也是我们公司项目中采用的方法!

Reference

http://preshing.com/20130930/double-checked-locking-is-fixed-in-cpp11/

C++ and the Perils of Double-Checked Locking

时间: 2024-10-19 00:17:13

设计模式系列之单例模式的相关文章

23种设计模式系列之单例模式

本文继续介绍23种设计模式系列之单例模式. 概念: Java中单例模式是一种常见的设计模式,单例模式的写法有好几种,这里主要介绍三种:懒汉式单例.饿汉式单例.登记式单例. 单例模式有以下特点: 1.单例类只能有一个实例. 2.单例类必须自己创建自己的唯一实例. 3.单例类必须给所有其他对象提供这一实例. 单例模式确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例.在计算机系统中,线程池.缓存.日志对象.对话框.打印机.显卡的驱动程序对象常被设计成单例.这些应用都或多或少具有资源管理器

[js高手之路]设计模式系列课程-单例模式实现模态框

什么是单例呢? 单,就是一个的意思.例:就是实例化出来的对象,那合在一起就是保证一个构造函数只能new出一个实例,为什么要学习单例模式呢?或者说单例模式有哪些常见的应用场景.它的使用还是很广泛,比如:弹出一个模态框,一般来说在网站中弹出的模态框,不停的一直点击,一般只能创建一个.还有后台的数据库连接,一般都是保证一个连接等等.今天的主题就是单例在模态框中的应用,我们先要搞清楚,怎么弄个单例出来. 我们先看下普通的构造函数加原型方式.下面这种是常见的方式 1 function Singleton

【设计模式系列】单例模式的7种写法

前言 单例模式是一种常用的软件设计模式,在他的核心结构中只包含一个被称为 单例的特殊类.通过单例模式可以保证系统只有一个实例而且该实例易于外界访问,从而方便对实例个数的控制并节约系统资源.如果希望在系统中某个类的对象只能存在一个,单例模式是最好的解决方案. Singletom类,定义一个GetInstance操作,允许客户访问他的唯一实例,GetInstance是一个静态方法,主要负责创建自己的唯一实例. 一.实现 1.饿汉式 public class Singleton { private s

Java设计模式系列之单例模式

单例模式的定义 一个类有且仅有一个实例,并且自行实例化向整个系统提供.比如,多程序读取一个配置文件时,建议配置文件时,建议配置文件封装成对象.会方便操作其中的数据,又要保证多个程序读到的是同一个配置文件对象,就需要该配置文件对象在内存中是唯一的. 单例模式的作用 简单说来,单例模式(也叫单件模式)的作用就是保证在整个应用程序的生命周期中,任何一个时刻,单例类的实例都只存在一个(当然也可以不存在). 单例模式的类图 如何保证对象的唯一性 思想:(1)不让其他程序创建该类对象; (2)在本类中创建一

[js高手之路]设计模式系列课程-组合模式+寄生组合继承实战新闻列表

所谓组合模式,就是把一堆结构分解出来,组成在一起,现实中很多这样的例子,如: 1.肯德基套餐就是一种组合模式, 比如鸡腿堡套餐,一般是是由一个鸡腿堡,一包薯条,一杯可乐等组成的 2.组装的台式机同理,由主板,电源,内存条,显卡, 机箱,显示器,外设等组成的 把一个成型的产品组成部件,分成一个个独立的部件,这种方式可以做出很多灵活的产品,这就是组合模式的优势 比如:家用台式机电脑,要求配置比较低, 这个时候只需要主板+电源+内存条+机箱+显示器+外设就可以了,不需要配置独立显卡 鸡腿堡+鸡翅+紫薯

设计模式总结篇系列:单例模式(SingleTon)

在Java设计模式中,单例模式相对来说算是比较简单的一种构建模式.适用的场景在于:对于定义的一个类,在整个应用程序执行期间只有唯一的一个实例对象.如Android中常见的Application对象. 通过单例模式,自行实例化并向这个系统提供这个单一实例的访问方法. 根据此单一实例产生的时机不同(当然,都是指第一次,也是唯一一次产生此单一实例时),可以将其分为懒汉式.饿汉式和登记式. 一.懒汉式: 其特点是延迟加载,即当需要用到此单一实例的时候,才去初始化此单一实例.常见经典的写法如下: 1 pa

Java 设计模式系列(五)单例模式

Java 设计模式系列(五)单例模式 单例模式确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例. 一.懒汉式单例 /** * 懒汉式单例类.在第一次调用的时候实例化自己 * 1. 构造器私有化,避免外面直接创建对象 * 2. 声明一个私有的静态变量 * 3. 创建一个对外的公共静态方法访问该变量,如果没有变量就创建对象 */ public class Singleton { private Singleton() throws InterruptedException { Thre

深入浅出设计模式系列 -- 单例模式

注:本文出自博主:chloneda 前言 深入浅出设计模式系列,尽量采用通俗易懂.循序渐进的方式,让大家真正理解设计模式的精髓! 单例模式知识点 在开始讲解单例模式之前,先了解一下单例模式的知识点. 单例模式定义:确保一个类只有一个自行实例化的实例,并提供一个全局访问点,向整个系统提供这个实例. 模式类型:创建类模式 单例模式类图: 单例模式可以简单总结为三个要素: 私有的构造方法. 提供私有的.静态的.指向当前实例的引用. 提供公有的.静态的方法 即全局访问点以返回这个实例. 单例模式的本质:

【白话设计模式四】单例模式(Singleton)

转自:https://my.oschina.net/xianggao/blog/616385 0 系列目录 白话设计模式 工厂模式 单例模式 [白话设计模式一]简单工厂模式(Simple Factory) [白话设计模式二]外观模式(Facade) [白话设计模式三]适配器模式(Adapter) [白话设计模式四]单例模式(Singleton) [白话设计模式五]工厂方法模式(Factory Method) [白话设计模式六]抽象工厂模式(Abstract Factory) [白话设计模式七]策