浅谈C#单例模式的实现和性能对比

 更新时间:2020年6月25日 11:18  点击:1962

简介

单例指的是只能存在一个实例的类(在C#中,更准确的说法是在每个AppDomain之中只能存在一个实例的类,它是软件工程中使用最多的几种模式之一。在第一个使用者创建了这个类的实例之后,其后需要使用这个类的就只能使用之前创建的实例,无法再创建一个新的实例。通常情况下,单例会在第一次被使用时创建。本文会对C#中几种单例的实现方式进行介绍,并分析它们之间的线程安全性和性能差异。

单例的实现方式有很多种,但从最简单的实现(非延迟加载,非线程安全,效率低下),到可延迟加载,线程安全,且高效的实现,它们都有一些基本的共同点:

  • 单例类都只有一个private的无参构造函数
  • 类声明为sealed(不是必须的)
  • 类中有一个静态变量保存着所创建的实例的引用
  • 单例类会提供一个静态方法或属性来返回创建的实例的引用(eg.GetInstance)

几种实现

一非线程安全

//Bad code! Do not use!
public sealed class Singleton
{
  private static Singleton instance = null;

  private Singleton()
  {

  }

  public static Singleton instance
  {
    get
    {
      if (instance == null)
      {
        instance = new Singleton();
      }
      return instance;
    }
  }
}

这种方法不是线程安全的,会存在两个线程同时执行if (instance == null)并且创建两个不同的instance,后创建的会替换掉新创建的,导致之前拿到的reference为空。

二简单的线程安全实现

public sealed class Singleton
{
  private static Singleton instance = null;
  private static readonly object padlock = new object();

  Singleton()
  {
  }

  public static Singleton Instance
  {
    get
    {
      lock (padlock)
      {
        if (instance == null)
        {
          instance = new Singleton();
        }
        return instance;
      }
    }
  }
}

相比较于实现一,这个版本加上了一个对instance的锁,在调用instance之前要先对padlock上锁,这样就避免了实现一中的线程冲突,该实现自始至终只会创建一个instance了。但是,由于每次调用Instance都会使用到锁,而调用锁的开销较大,这个实现会有一定的性能损失。

注意这里我们使用的是新建一个private的object实例padlock来实现锁操作,而不是直接对Singleton进行上锁。直接对类型上锁会出现潜在的风险,因为这个类型是public的,所以理论上它会在任何code里调用,直接对它上锁会导致性能问题,甚至会出现死锁情况。

Note: C#中,同一个线程是可以对一个object进行多次上锁的,但是不同线程之间如果同时上锁,就可能会出现线程等待,或者严重的会出现死锁情况。因此,我们在使用lock时,尽量选择类中的私有变量上锁,这样可以避免上述情况发生。

三双重验证的线程安全实现

public sealed calss Singleton
{
  private static Singleton instance = null;
  private static readonly object padlock = new object();

  Singleton()
  {
  }

  public static Singleton Instance
  {
    get
    {
      if (instance == null)
      {
        lock (padlock)
        {
          if (instance == null)
          {
            instance = new Singleton();
          }
        }
      }
      return instance;
    }
  } 
}

在保证线程安全的同时,这个实现还避免了每次调用Instance都进行lock操作,这会节约一定的时间。

但是,这种实现也有它的缺点:

1无法在Java中工作。(具体原因可以见原文,这边没怎么理解)

2程序员在自己实现时很容易出错。如果对这个模式的代码进行自己的修改,要倍加小心,因为double check的逻辑较为复杂,很容易出现思考不周而出错的情况。

四不用锁的线程安全实现

public sealed class Singleton
{
  //在Singleton第一次被调用时会执行instance的初始化
  private static readonly Singleton instance = new Singleton();

  //Explicit static consturctor to tell C# compiler 
  //not to mark type as beforefieldinit
  static Singleton()
  {
  }

  private Singleton()
  {
  }

  public static Singleton Instance
  {
    get
    {
      return instance;
    }
  }
}

这个实现很简单,并没有用到锁,但是它仍然是线程安全的。这里使用了一个static,readonly的Singleton实例,它会在Singleton第一次被调用的时候新建一个instance,这里新建时候的线程安全保障是由.NET直接控制的,我们可以认为它是一个原子操作,并且在一个AppDomaing中它只会被创建一次。

这种实现也有一些缺点:

1instance被创建的时机不明,任何对Singleton的调用都会提前创建instance
2static构造函数的循环调用。如有A,B两个类,A的静态构造函数中调用了B,而B的静态构造函数中又调用了A,这两个就会形成一个循环调用,严重的会导致程序崩溃。
3我们需要手动添加Singleton的静态构造函数来确保Singleton类型不会被自动加上beforefieldinit这个Attribute,以此来确保instance会在第一次调用Singleton时才被创建。
4readonly的属性无法在运行时改变,如果我们需要在程序运行时dispose这个instance再重新创建一个新的instance,这种实现方法就无法满足。

五完全延迟加载实现(fully lazy instantiation)

public sealed class Singleton
{
  private Singleton()
  {
  }

  public static Singleton Instance 
  {
    get
    {
      return Nested.instance;
    }
  }

  private class Nested
  {
    // Explicit static constructor to tell C# compiler
    // not to mark type as beforefieldinit
    static Nested()
    {
    }

    internal static readonly Singleton instance = new Singleton();
  }
}

实现五是实现四的包装。它确保了instance只会在Instance的get方法里面调用,且只会在第一次调用前初始化。它是实现四的确保延迟加载的版本。

六 使用.NET4的Lazy<T>类型

public sealed class Singleton
{
  private static readonly Lazy<Singleton> lazy = new Lazy<Singleton>(() => new Singleton());

  public static Singleton Instance 
  {
    get 
    {
      return lazy.Value;
    }
  }

  private Singleton()
  {
  }
}

.NET4或以上的版本支持Lazy<T>来实现延迟加载,它用最简洁的代码保证了单例的线程安全和延迟加载特性。

性能差异

之前的实现中,我们都在强调代码的线程安全性和延迟加载。然而在实际使用中,如果你的单例类的初始化不是一个很耗时的操作或者初始化顺序不会导致bug,延迟初始化是一个可有可无的特性,因为初始化所占用的时间是可以忽略不计的。

在实际使用场景中,如果你的单例实例会被频繁得调用(如在一个循环中),那么为了保证线程安全而带来的性能消耗是更值得关注的地方。

为了比较这几种实现的性能,我做了一个小测试,循环拿这些实现中的单例9亿次,每次调用instance的方法执行一个count++操作,每隔一百万输出一次,运行环境是MBP上的Visual Studio for Mac。结果如下:

线程安全性 延迟加载 测试运行时间(ms)
实现一 15532
实现二 45803
实现三 15953
实现四 不完全 14572
实现五 14295
实现六 22875

测试方法并不严谨,但是仍然可以看出,方法二由于每次都需要调用lock,是最耗时的,几乎是其他几个的三倍。排第二的则是使用.NET Lazy类型的实现,比其他多了二分之一左右。其余的四个,则没有明显区别。

总结

总体来说,上面说的多种单例实现方式在现今的计算机性能下差距都不大,除非你需要特别大并发量的调用instance,才会需要去考虑锁的性能问题。

对于一般的开发者来说,使用方法二或者方法六来实现单例已经是足够好的了,方法四和五则需要对C#运行流程有一个较好的认识,并且实现时需要掌握一定技巧,并且他们节省的时间仍然是有限的。

引用

本文大部分是翻译自Implementing the Singleton Pattern in C#,加上了一部分自己的理解。这是我搜索static readonly field initializer vs static constructor initialization时看到的,在这里对两位作者表示感谢。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持猪先飞。

[!--infotagslink--]

相关文章

  • C#实现简单的登录界面

    我们在使用C#做项目的时候,基本上都需要制作登录界面,那么今天我们就来一步步看看,如果简单的实现登录界面呢,本文给出2个例子,由简入难,希望大家能够喜欢。...2020-06-25
  • 浅谈C# 字段和属性

    这篇文章主要介绍了C# 字段和属性的的相关资料,文中示例代码非常详细,供大家参考和学习,感兴趣的朋友可以了解下...2020-11-03
  • C#中截取字符串的的基本方法详解

    这篇文章主要介绍了C#中截取字符串的的基本方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2020-11-03
  • C#实现简单的Http请求实例

    这篇文章主要介绍了C#实现简单的Http请求的方法,以实例形式较为详细的分析了C#实现Http请求的具体方法,需要的朋友可以参考下...2020-06-25
  • C#连接SQL数据库和查询数据功能的操作技巧

    本文给大家分享C#连接SQL数据库和查询数据功能的操作技巧,本文通过图文并茂的形式给大家介绍的非常详细,需要的朋友参考下吧...2021-05-17
  • C#中new的几种用法详解

    本文主要介绍了C#中new的几种用法,具有很好的参考价值,下面跟着小编一起来看下吧...2020-06-25
  • 使用Visual Studio2019创建C#项目(窗体应用程序、控制台应用程序、Web应用程序)

    这篇文章主要介绍了使用Visual Studio2019创建C#项目(窗体应用程序、控制台应用程序、Web应用程序),小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧...2020-06-25
  • C#开发Windows窗体应用程序的简单操作步骤

    这篇文章主要介绍了C#开发Windows窗体应用程序的简单操作步骤,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2021-04-12
  • C#从数据库读取图片并保存的两种方法

    这篇文章主要介绍了C#从数据库读取图片并保存的方法,帮助大家更好的理解和使用c#,感兴趣的朋友可以了解下...2021-01-16
  • C#和JavaScript实现交互的方法

    最近做一个小项目不可避免的需要前端脚本与后台进行交互。由于是在asp.net中实现,故问题演化成asp.net中jiavascript与后台c#如何进行交互。...2020-06-25
  • C++调用C#的DLL程序实现方法

    本文通过例子,讲述了C++调用C#的DLL程序的方法,作出了以下总结,下面就让我们一起来学习吧。...2020-06-25
  • 轻松学习C#的基础入门

    轻松学习C#的基础入门,了解C#最基本的知识点,C#是一种简洁的,类型安全的一种完全面向对象的开发语言,是Microsoft专门基于.NET Framework平台开发的而量身定做的高级程序设计语言,需要的朋友可以参考下...2020-06-25
  • C#变量命名规则小结

    本文主要介绍了C#变量命名规则小结,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...2021-09-09
  • C#绘制曲线图的方法

    这篇文章主要介绍了C#绘制曲线图的方法,以完整实例形式较为详细的分析了C#进行曲线绘制的具体步骤与相关技巧,具有一定参考借鉴价值,需要的朋友可以参考下...2020-06-25
  • C# 中如何取绝对值函数

    本文主要介绍了C# 中取绝对值的函数。具有很好的参考价值。下面跟着小编一起来看下吧...2020-06-25
  • c#自带缓存使用方法 c#移除清理缓存

    这篇文章主要介绍了c#自带缓存使用方法,包括获取数据缓存、设置数据缓存、移除指定数据缓存等方法,需要的朋友可以参考下...2020-06-25
  • c#中(&&,||)与(&,|)的区别详解

    这篇文章主要介绍了c#中(&&,||)与(&,|)的区别详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2020-06-25
  • 经典实例讲解C#递归算法

    这篇文章主要用实例讲解C#递归算法的概念以及用法,文中代码非常详细,帮助大家更好的参考和学习,感兴趣的朋友可以了解下...2020-06-25
  • C#学习笔记- 随机函数Random()的用法详解

    下面小编就为大家带来一篇C#学习笔记- 随机函数Random()的用法详解。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧...2020-06-25
  • C#中list用法实例

    这篇文章主要介绍了C#中list用法,结合实例形式分析了C#中list排序、运算、转换等常见操作技巧,具有一定参考借鉴价值,需要的朋友可以参考下...2020-06-25