《C#设计模式》- 单例模式
深度解析六种单例模式实现方法,以及安全,性能分析
第一种:不是线程安全的
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)
并发现它为true
,然后两个都创建实例,这违反了单例模式。请注意,实际上,在计算表达式之前可能已经创建了实例,但是内存模型不保证其他线程可以看到实例的新值,除非已经传递了合适的内存屏障(互斥锁)
第二种 :简单线程安全
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;}}} }
此实现是线程安全的。线程取消对共享对象的锁定,然后在创建实例之前检查是否已创建实例。这会解决内存屏障问题(因为锁定确保在获取锁之后逻辑上发生所有读取,并且解锁确保在锁定释放之前逻辑上发生所有写入)并确保只有一个线程将创建实例(仅限于一次只能有一个线程可以在代码的那一部分中——当第二个线程进入它时,第一个线程将创建实例,因此表达式将计算为
false
)。不幸的是,每次请求实例时都会获得锁定,因此性能会受到影响。请注意,不是
typeof(Singleton)
像这个实现的某些版本那样锁定,而是锁定一个私有的静态变量的值。锁定其他类可以访问和锁定的对象(例如类型)会导致性能问题甚至死锁。这是我通常的风格偏好——只要有可能,只锁定专门为锁定目的而创建的对象,或者为了特定目的(例如,等待/触发队列)而锁定的文档。通常这些对象应该是它们所使用的类的私有对象。这有助于使编写线程安全的应用程序变得更加容易。
第三种:双重检查锁定尝试线程安全
public sealed class 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;}} }
该实现尝试是线程安全的,而不必每次都取出锁。不幸的是,该模式有四个缺点:
- 它在Java中不起作用。这似乎是一个奇怪的事情,但是如果您需要Java中的单例模式,这是值得知道的,C#程序员也可能是Java程序员。Java内存模型无法确保构造函数在将新对象的引用分配给
Instance
之前完成。Java内存模型经历了1.5版本的重新改进,但是在没有volatile变量(如在C#中)的情况下,双重检查锁定仍然会被破坏。- 在没有任何内存障碍的情况下,ECMA CLI规范也打破了这一限制。有可能在.NET 2.0内存模型(比ECMA规范更强)下它是安全的,但我宁愿不依赖那些更强大的语义,特别是如果对安全性有任何疑问的话。使
instance
变量volatile变得有效,就像明确的内存屏障调用一样,尽管在后一种情况下,甚至专家也无法准确地就需要哪些屏障达成一致。我尽量避免专家对对错意见也不一致的情况!- 这很容易出错。该模式需要完全如上所述——任何重大变化都可能影响性能或正确性。
- 它的性能仍然不如后续的实现。
第四种:饱汉式,不使用锁且线程安全
public sealed class Singleton {private static readonly Singleton instance = new Singleton();// 显式静态构造函数告诉C#编译器static Singleton(){}private Singleton(){}public static Singleton Instance{get{return instance;}} }
C#中的静态构造函数仅在创建类的实例或引用静态成员时执行,并且每个AppDomain只执行一次。考虑到无论发生什么情况,都需要执行对新构造的类型的检查,这比在前面的示例中添加额外检查要快。然而,还有一些小缺陷:
- 它并不像其他实现那样懒惰。特别是,如果您有
Instance
之外的静态成员,那么对这些成员的第一次引用将涉及到创建实例。这将在下一个实现中得到纠正。- 如果一个静态构造函数调用另一个静态构造函数,而另一个静态构造函数再次调用第一个构造函数,则会出现复杂情况。
- 类型初始化器的懒惰性只有在.NET没有使用名为
BeforeFieldInit
的特殊标志标记类型时才能得到保证。不幸的是,C#编译器,将所有没有静态构造函数的类型。
第五种:饿汉式,锁且线程安全
public sealed class Singleton {private Singleton(){}public static Singleton Instance { get { return Nested.instance; } }private class Nested{// 显式静态构造告诉C#编译器static Nested(){}internal static readonly Singleton instance = new Singleton();} }
在这里,实例化是由对嵌套类的静态成员的第一次引用触发的,该引用只发生在
Instance
中。这意味着实现是完全懒惰的,但是具有前面实现的所有性能优势。请注意,尽管嵌套类可以访问封闭类的私有成员,但反之则不然,因此需要instance
在此处为内部成员。不过,这不会引起任何其他问题,因为类本身是私有的。但是,为了使实例化变得懒惰,代码要稍微复杂一些。
第六种:使用.NET 4的 Lazy 类型【延迟加载对象】
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(){} }
它很简单,而且性能很好。它还允许您检查是否已使用IsValueCreated 属性创建实例(如果需要的话)。
上面的代码隐式地将
LazyThreadSafetyMode.ExecutionAndPublication
用作Lazy<Singleton>
的线程安全模式。根据您的要求,您可能希望尝试其他模式。
本文案例分析来源