C# 之单例模式
一、介绍:
- 单例模式是软件工程中最著名的模式之一。从本质上来讲,单例是一个允许创建自身的单个实例的类,并且通常可以简单的访问该实例。
- 单例不允许在创建实例时指定任何参数。
- 单例通常要求他们是懒惰的创建,即直到第一次需要时才创建实例。
单例的实现,有四个共同特征:
- 单个构造函数,它是私有且无参数的。这可以防止其他类实例化它。
- 类是密封的。严格来说,由于上述原因,这个不是必要的,但是可以帮助JIT进行更多的优化。
- 一个静态变量,用于保存对单个已创建实例的引用。(如果有的话)
- 公共静态意味着获取对单个已创建实例的引用,必要时创建一个实例。
请注意:所有这些实现还使用公共静态属性Instance 作为访问实例的方法。在所有情况下,可以轻松将属性转换为方法,而不会影响线程安全和性能。
二、单例的6个常见版本:
- 第一个版本:不是线程安全的:(糟糕的代码,请勿使用)
public sealed class Singleton1 { private static Singleton1 instance = null; private Singleton1() { } public static Singleton1 Instance { get { if (instance == null) { instance = new Singleton1(); } return instance; } } }
上面代码,不是线程安全的,两个不同线程都去竞争 if(instance == null) 时候,然后发现为true,会同时创建两个实例,这违反了单例原则。为了防止这种情况,我们想到了互斥锁,保证两个线程不会共同创建实例,请看第二个版本。
- 第二个版本:简单的线程安全
public sealed class Singleton2 { private static Singleton2 instance = null; private static readonly object padlock = new object(); Singleton2() { } public static Singleton2 Instance { get { lock (padlock)//加锁,保证两个线程并发时,不会重复创建对象,但问题是每次线程运行还得检查锁(性能略差) { if (instance == null) { instance = new Singleton2(); } return instance; } } } }
上述实现时线程安全的,加锁之后,防止多线程并发创建重复对象,但它有性能缺陷,因为每次使用Instance 调用实例时,都需要去判断锁,这样导致性能有影响,于是我们想到了第三个版本,双重判断加锁。
- 第三个版本,使用双重检查锁定来尝试线程安全(糟糕的代码,请勿使用)
public sealed class Singleton3 { private static Singleton3 instance = null; private static readonly object padlock = new object(); Singleton3() { } public static Singleton3 Instance { get { if (instance == null) { lock (padlock) { if (instance == null) { instance = new Singleton3(); } } } return instance; } } }
该实现是线程安全的,不必每次都取出锁,即当第一次创建完实例后,下一次在调用实例,则不必再取出锁了。但该模式有四个缺点
- 它在Java中不起作用。
- 在没有任何内存障碍的情况下,ECMA CLI规范也打破了这一限制。
- 这很容易出错。该模式需要完全如上所述,任何重大变化都可能影响性能或正确性。
- 它的性能不如后续实现。
- 第四个版本,不太懒,不使用锁且线程安全
public sealed class Singleton4 { private static readonly Singleton4 instance = new Singleton4(); static Singleton4() { }//显示静态构造函数告诉C# 编译器 private Singleton4() { } public static Singleton4 Instance { get { return instance; } } }
为什么说他不太懒惰?因为所谓懒惰是指我们要在调用实例时才去判断是否创建实例instance,然而这里,在类里先创建了实例。
为什么说他是线程安全的?因为 C# 中静态构造函数仅在 创建类的实例 或 引用类的静态成员时执行,举个例子,例如我在调用Instance 这个实例时,会执行第一句 instance = new Singleton4(),当我第二次再调用Instance 使用静态实例时,它则不会重复创建实例,所以它是线程安全的。
- 第五个版本,完全懒惰实例化
public sealed class Singleton5 { private Singleton5() { } public static Singleton5 Instance { get { return Nested.instance; } }//完全懒惰的,因为必须在执行这个Instance调用时,才会执行嵌套类的创建单例语句 private class Nested { static Nested() { } internal static readonly Singleton5 instance = new Singleton5();//此处要为internal的,外部要访问,不能是私有的。此处在外部调用时,需要创建单例对象时创建,实现完全懒惰 } }
上述代码是懒惰的,因为在封闭类中调用子类的instance时才去创建实例,而不是像第四个版本那样先创建了实例。请注意,尽管嵌套类可以访问封闭类中的私有成员,但是封闭类不能访问嵌套类内层,所以这里的嵌套类实例需要使用internal 关键字声明。
- 第六个版本:使用.NET 4 的Lazy 类型。
public sealed class Singleton6 { private static readonly Lazy<Singleton6> lazy = new Lazy<Singleton6>(() => new Singleton6()); public static Singleton6 Instance { get { return lazy.Value; } } private Singleton6() { } }
这里使用.NET4 或者更高版本,可以使用System.Lazy 这个类型声明懒惰的,线程安全的单例,同时他的性能非常好。
三、懒惰与性能
在许多情况下,其实不需要完全懒惰,除非您的初始化做了一些特别耗时的事情,或者其他地方产生了一些副作用,否则最好忽略上面所示的显示静态构造函数。这可以提高性能,因为它允许JIT编译器进行一次检查(例如在方法的开头)以确保类型已经初始化,然后从那时开始设定它。如果在相对紧密的循环中引用单例实例,则会产生(相对)显著的性能差异。您应该决定是否需要完全延迟实例化,并在类中适当地记录此决策。
参考原文:https://www.cnblogs.com/leolion/p/10241822.html