介绍

单例模式是软件工程中最著名的模式之一。本质上,单例是一个类,它仅允许创建其自身的单个实例,并且通常提供对该实例的简单访问。最常见的是,在创建实例时,单例不允许指定任何参数-否则,对实例的第二次请求但参数不同可能会出现问题!(如果应该为具有相同参数的所有请求访问相同的实例,则使用工厂模式更为合适。)本文仅涉及不需要参数的情况。通常,单例的要求是延迟创建它们-即,直到首次需要该实例时才创建实例。
在C#中有多种不同的方式来实现单例模式。我将在这里以相反的优雅顺序介绍它们,从最常见的线程安全性开始,到完全延迟加载,线程安全,简单且高性能的版本。
所有这些实现都有四个共同的特征,但是:

  • 单个构造函数,私有且无参数。这样可以防止其他类实例化它(这将违反模式)。请注意,它还防止了子类化-如果一个单例可以被子类化一次,则可以被子类化两次,并且如果每个子类都可以创建一个实例,则将违反该模式。如果您需要一个基本类型的实例,则可以使用工厂模式,但是直到运行时才知道确切的类型。
  • 该类是密封的。严格来说,由于上述几点,这是不必要的,但可以帮助JIT进行更多优化。
  • 一个静态变量,其中包含对创建的单个实例的引用(如果有)。
  • 公共静态方法是获取对单个已创建实例的引用,并在必要时创建一个实例。

请注意,所有这些实现也都使用公共静态属性Instance 作为访问实例的方式。在所有情况下,该属性都可以轻松转换为方法,而不会影响线程安全性或性能。
单例(Singleton)模式:确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。单例模式是一种对象创建模式。

单例模式(Singleton) 学习难度:★☆☆☆☆ 使用频率:★★★★☆

第一版-非线程安全

  1. // Bad code! Do not use!
  2. public sealed class Singleton
  3. {
  4. // 定义一个静态变量来保存类的实例
  5. private static Singleton instance = null;
  6. // 定义私有构造函数,使外界不能创建该类实例
  7. private Singleton()
  8. {
  9. }
  10. #region 定义公有方法或属性提供一个全局访问点(二者选其一)
  11. /// <summary>
  12. /// 1.定义公有属性来提供全局访问点
  13. /// </summary>
  14. /// <returns></returns>
  15. public static Singleton Instance
  16. {
  17. get
  18. {
  19. if (instance == null)
  20. {
  21. instance = new Singleton();
  22. }
  23. return instance;
  24. }
  25. }
  26. /// <summary>
  27. /// 2.定义公有方法提供一个全局访问点
  28. /// </summary>
  29. /// <returns></returns>
  30. public static Singleton GetInstance()
  31. {
  32. // 如果类的实例不存在则创建,否则直接返回
  33. if (instance == null)
  34. {
  35. instance = new Singleton();
  36. }
  37. return instance;
  38. }
  39. #endregion
  40. }

如前所述,以上内容不是线程安全的。两个不同的线程都可以评估测试if (instance==null)并发现测试是正确的,然后都创建实例,这违反了单例模式。请注意,实际上可能已经在计算表达式之前创建了实例,但是内存模型不能保证实例的新值将被其他线程看到,除非已传递适当的内存屏障。

饿汉式单例:懒汉式单例实现起来最为简单,在C#中,我们可以利用静态构造函数来实现。如下代码所示:

  1. public class LoadBalancer
  2. {
  3. // 私有静态变量,存储唯一实例
  4. private static readonly LoadBalancer instance = new LoadBalancer();
  5. ......
  6. // 公共静态成员方法,返回唯一实例
  7. public static LoadBalancer GetLoadBalancer()
  8. {
  9. return instance;
  10. }
  11. }

第二版-简单的线程安全

  1. public sealed class Singleton
  2. {
  3. // 定义一个静态变量来保存类的实例
  4. private static Singleton instance = null;
  5. // 定义一个标识确保线程同步
  6. private static readonly object padlock = new object();
  7. // 定义私有构造函数,使外界不能创建该类实例
  8. Singleton()
  9. {
  10. }
  11. #region 定义公有方法或属性提供一个全局访问点(二者选其一)
  12. /// <summary>
  13. /// 1.定义公有属性来提供全局访问点
  14. /// </summary>
  15. /// <returns></returns>
  16. public static Singleton Instance
  17. {
  18. get
  19. {
  20. lock (padlock)
  21. {
  22. if (instance == null)
  23. {
  24. instance = new Singleton();
  25. }
  26. return instance;
  27. }
  28. }
  29. }
  30. /// <summary>
  31. /// 2.定义公有方法提供一个全局访问点
  32. /// </summary>
  33. /// <returns></returns>
  34. public static Singleton GetInstance()
  35. {
  36. // 当第一个线程运行到这里时,此时会对locker对象 "加锁",
  37. // 当第二个线程运行该方法时,首先检测到locker对象为"加锁"状态,该线程就会挂起等待第一个线程解锁
  38. // lock语句运行完之后(即线程运行完之后)会对该对象"解锁"
  39. lock (locker)
  40. {
  41. // 如果类的实例不存在则创建,否则直接返回
  42. if (instance == null)
  43. {
  44. instance = new Singleton();
  45. }
  46. }
  47. return instance;
  48. }
  49. #endregion
  50. }

此实现是线程安全的。该线程在共享对象上获取一个锁,然后在创建实例之前检查是否已创建该实例。这可以解决内存屏障问题(因为锁定可确保所有读取均在获取锁之后逻辑发生,而解锁则确保所有写入均在锁释放之前逻辑发生)并确保只有一个线程将创建一个实例(仅一个线程一次可以位于代码的该部分中-到第二个线程进入该线程时,第一个线程将创建实例,因此该表达式的值将为false)。不幸的是,每次请求实例时都需要获取锁,因此性能会受到影响。
请注意typeof(Singleton),我没有锁定该实现的某些版本,而是锁定了该类私有的静态变量的值。锁定其他类可以访问和锁定的对象(例如类型)可能会导致性能问题甚至死锁。这是我的一般样式偏爱-尽可能仅锁定专门为锁定目的而创建的对象,或者出于特定目的(例如,等待/发出队列)而将其锁定在哪个文档上。通常,此类对象应为使用它们的类所专用。这有助于使编写线程安全的应用程序变得更加容易。
懒汉式单例:在第一个调用Singleton.GetInstance()时才会实例化对象,这种技术又被称之为延迟加载(Lazy Load)

第三版-使用双重检查锁定尝试线程安全

双重检查锁定(Double-Checked Locking)

  1. // Bad code! Do not use!
  2. public sealed class Singleton
  3. {
  4. // 定义一个静态变量来保存类的实例
  5. private static Singleton instance = null;
  6. // 定义一个标识确保线程同步
  7. private static readonly object padlock = new object();
  8. // 定义私有构造函数,使外界不能创建该类实例
  9. Singleton()
  10. {
  11. }
  12. #region 定义公有方法或属性提供一个全局访问点(二者选其一)
  13. /// <summary>
  14. /// 1.定义公有属性来提供全局访问点
  15. /// </summary>
  16. /// <returns></returns>
  17. public static Singleton Instance
  18. {
  19. get
  20. {
  21. // 第一重判断
  22. if (instance == null)
  23. {
  24. // 锁定代码块
  25. lock (padlock)
  26. {
  27. // 第二重判断
  28. if (instance == null)
  29. {
  30. instance = new Singleton();
  31. }
  32. }
  33. }
  34. return instance;
  35. }
  36. }
  37. /// <summary>
  38. /// 2.定义公有方法提供一个全局访问点
  39. /// </summary>
  40. /// <returns></returns>
  41. public static Singleton GetInstance()
  42. {
  43. // 当第一个线程运行到这里时,此时会对locker对象 "加锁",
  44. // 当第二个线程运行该方法时,首先检测到locker对象为"加锁"状态,该线程就会挂起等待第一个线程解锁
  45. // lock语句运行完之后(即线程运行完之后)会对该对象"解锁"
  46. // 双重检查锁定只需要一句判断就可以了
  47. if (instance == null)
  48. {
  49. lock (locker)
  50. {
  51. // 如果类的实例不存在则创建,否则直接返回
  52. if (instance == null)
  53. {
  54. instance = new Singleton();
  55. }
  56. }
  57. }
  58. return instance;
  59. }
  60. #endregion
  61. }

此实现尝试是线程安全的,而不必每次都取出锁。不幸的是,该模式有四个缺点:

  • 它在Java中不起作用。评论这似乎有些奇怪,但是值得一提的是,是否需要Java中的单例模式,C#程序员也很可能是Java程序员。Java内存模型不能确保在将对新对象的引用分配给实例之前,构造函数已完成。Java内存模型针对1.5版进行了重新加工,但是在此之后,在没有易失性变量的情况下,双重检查锁定仍然无效(如C#)。
  • 没有任何内存障碍,它在ECMA CLI规范中也被打破。在.NET 2.0内存模型(比ECMA规范更强)下,它很可能是安全的,但我宁愿不依赖那些更强的语义,尤其是在对安全性有任何疑问的情况下。将instance变量设置为volatile可以使其工作,就像显式的内存屏障调用一样,尽管在后一种情况下,即使专家也无法确切地确定需要哪些屏障。我倾向于尝试避免专家不同意对错的情况!
  • 很容易出错。模式必须与上面的完全一样-任何重大更改都可能影响性能或正确性。
  • 它的性能仍然不如后来的实现。

    第四版-并不那么懒,但是在不使用锁的情况下是线程安全的

  1. public sealed class Singleton
  2. {
  3. private static readonly Singleton instance = new Singleton();
  4. // Explicit static constructor to tell C# compiler
  5. // not to mark type as beforefieldinit
  6. //静态构造函数,CLR只执行一次
  7. static Singleton()
  8. {
  9. }
  10. //私有构造函数,防止外界调用
  11. private Singleton()
  12. {
  13. }
  14. public static Singleton Instance
  15. {
  16. get
  17. {
  18. return instance;
  19. }
  20. }
  21. }

如您所见,这确实非常简单-但是为什么它是线程安全的,它有多懒呢?好吧,将C#中的静态构造函数指定为仅在创建类的实例或引用静态成员时执行,并且每个AppDomain仅执行一次。鉴于无论其他情况如何都需要执行对新构造的类型的检查,因此比在前面的示例中添加额外的检查要快。但是,有一些缺点:

  • 它不像其他实现那么懒。特别是,如果您拥有以外的静态成员Instance,则对这些成员的首次引用将涉及创建实例。在下一个实现中将对此进行更正。
  • 如果一个静态构造函数调用另一个静态构造函数,而另一个又调用第一个静态构造函数,则会带来麻烦。请查阅.NET规范(当前位于分区II的9.5.3节),以获取有关类型初始值设定项的确切性质的更多详细信息-它们不太可能会叮咬您,但是值得一提的是,静态构造函数会引用每个类型的初始值其他在一个周期中。
  • 只有当类型未使用称为的特殊标志标记时,.NET才能保证类型初始值设定项的惰性beforefieldinit。不幸的是,C#编译器(至少在.NET 1.1运行时中提供)将所有没有静态构造函数(即,看起来像构造函数但被标记为静态的块)的类型都标记为beforefieldinit。我现在有一篇 文章,详细介绍了这个问题 。还要注意,它会影响性能,如页面底部所述。

使用此实现(仅此实现)可以采取的一种捷径是仅创建 instance一个公共的静态只读变量,并完全摆脱该属性。这使得基本框架代码绝对很小!但是,许多人更喜欢拥有财产,以防将来需要采取进一步的行动,并且JIT内联法可能会使性能保持一致。(请注意,如果您需要懒惰,则仍然需要静态构造函数本身。)

第五版-完全延迟(懒惰)实例化

  1. public sealed class Singleton
  2. {
  3. //私有构造函数,防止外界调用
  4. private Singleton()
  5. {
  6. }
  7. public static Singleton Instance { get { return Nested.instance; } }
  8. // 使用内部类+静态构造函数实现延迟初始化
  9. private class Nested
  10. {
  11. // Explicit static constructor to tell C# compiler
  12. // not to mark type as beforefieldinit
  13. //静态构造函数,CLR只执行一次
  14. static Nested()
  15. {
  16. }
  17. internal static readonly Singleton instance = new Singleton();
  18. }
  19. }

在此,实例化是由对嵌套类的静态成员的第一次引用触发的,该引用仅在中出现Instance。这意味着该实现完全是懒惰的,但是具有先前实现的所有性能优势。请注意,尽管嵌套类可以访问封闭类的私有成员,但事实并非如此,因此需要instance在内部进行嵌套。但是,由于类本身是私有的,所以这不会引起任何其他问题。但是,为了使实例化变得懒惰,代码有些复杂。

第六版-使用.NET 4的Lazy<T>类型

如果您使用的是.NET 4(或更高版本),则可以使用System.Lazy 类型使延迟变得非常简单。您需要做的就是将一个委托传递给构造函数,该构造函数调用Singleton构造函数-使用lambda表达式最容易完成。

  1. public sealed class Singleton
  2. {
  3. private static readonly Lazy<Singleton>
  4. lazy =
  5. new Lazy<Singleton>
  6. (() => new Singleton());
  7. public static Singleton Instance { get { return lazy.Value; } }
  8. private Singleton()
  9. {
  10. }
  11. }

它很简单,性能也很好。如果需要,它还允许您检查是否已使用IsValueCreated 属性创建实例。
上面的代码隐式地LazyThreadSafetyMode.ExecutionAndPublication用作的线程安全模式Lazy<Singleton>。根据您的要求,您可能希望尝试其他模式。

性能与懒惰

在许多情况下,您实际上并不需要完全的惰性-除非您的类初始化做一些特别耗时的操作,或者在其他地方有副作用,否则可以忽略上面显示的显式静态构造函数。这可以提高性能,因为它允许JIT编译器进行一次检查(例如,在方法开始时进行检查),以确保类型已初始化,然后从此开始进行假定。如果您的单例实例是在相对紧凑的循环中引用的,则这可能会(相对)产生明显的性能差异。您应该确定是否需要完全延迟的实例化,并在类中适当记录此决定。
该页面存在的许多原因是人们试图变得聪明,因此提出了双重检查的锁定算法。锁的态度是昂贵的,这是普遍的并且被误导了。我编写了一个非常快速的基准测试,它以十亿种方式尝试各种变体,以循环方式获取单例实例。这并不是十分科学,因为在现实生活中,您可能想知道,如果每次迭代实际上都涉及到对获取单例的方法的调用等,该过程有多快。但是,它确实显示了重要的意义。在我的笔记本电脑上,最慢的解决方案(约为5倍)是锁定解决方案(解决方案2)。那重要吗?当您牢记它仍然设法收购了十亿美元的单身人士时,可能不会时间不到40秒。(注意:本文最初是在很早以前写的-我希望现在可以有更好的性能。)这意味着,如果您“仅”每秒获取40万次单例,则获取的成本将不断增加达到性能的1%-因此改善性能并不会带来太大的作用。现在,如果您经常 获取单身人士-难道您不是在循环中使用它吗?如果您非常在乎提高性能,为什么不在循环之外声明局部变量,请获取一次单例然后循环。宾果游戏,即使是最慢的实现也很容易做到。
我将非常有兴趣看到一个现实世界的应用程序,在该应用程序中,使用简单锁定和使用较快速的解决方案之一之间的差异实际上会产生明显的性能差异。

例外

有时,您需要在单例构造函数中进行工作,这可能会引发异常,但对整个应用程序可能不会致命。潜在地,您的应用程序可能能够解决问题,并希望重试。在这个阶段,使用类型初始化器构造单例成为问题。不同的运行时对这种情况的处理方式不同,但是我不知道哪个运行者可以做所需的事情(再次运行类型初始化器),即使这样做,您的代码也会在其他运行时中损坏。为了避免这些问题,我建议使用页面上列出的第二种模式-只需使用一个简单的锁,然后每次都进行检查,如果尚未成功构建该实例,则可以在方法/属性中进行构建。
感谢Andriy Tereshchenko提出了这个问题。

结论(2006年1月7日稍作修改; 2011年2月12日更新)

在C#中有多种不同的方式来实现单例模式。读者写信给我,详细介绍了他封装同步方面的一种方式,尽管我承认这在某些非常特殊的情况下(特别是在您想要非常高性能的情况下,并且能够确定单例是否已经被使用的能力)很有用。创建,并且完全懒惰,而与调用其他静态成员无关)。我个人认为这种情况不会经常出现,值得在此页面上进行进一步介绍,但是如果您遇到这种情况,请 发邮件给我
我的个人偏好是解决方案4:通常,我唯一会离开的地方是是否需要能够在不触发初始化的情况下调用其他静态方法,或者是否需要知道单例是否已被实例化。我不记得我上次处于那种情况,即使我有。在这种情况下,我可能会选择解决方案2,该解决方案仍然很不错,而且很容易就可以正确实现。
解决方案5优雅,但比2或4棘手,正如我上面所说,它提供的好处似乎很少有用。如果您使用的是.NET 4,则解决方案6是实现懒惰的一种更简单的方法,它还具有明显的惰性。我目前仍倾向于仅通过习惯就使用解决方案4,但如果我与经验不足的开发人员一起工作,我很可能会选择解决方案6作为一种简单且普遍适用的模式开始。
(我不会使用解决方案1,因为它已损坏,并且我不会使用解决方案3,因为它没有超过5的好处。)
本文翻译自Implementing the Singleton Pattern in C#,并结合EdisonZhou可均可可