简介
单例指的是只能存在一个实例的类(在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时看到的,在这里对两位作者表示感谢。
以上就是C#单例形式的完成以及机能对照的实例的细致内容,更多请关注ki4网别的相干文章!