单例模式分析
简单说来,单例模式(也叫单件模式)的作用就是保证在整个应用程序的生命周期中,
任何一个时刻,单例类的实例都只存在一个(当然也可以不存在)。
需求:
为什么要有单例模式呢,需求才是最根本的原因。那么究竟是为什么呢。
场景1.:需要我们做个winform的用户管理信息系统,那么我们需要有注册吧,ok,添加个注册按钮,点击注册按钮是不是应该弹出个注册的界面呢。
假如你做好了,你的boss来检查,好吧,注册一下吧,一点一个注册界面弹出来了,哎,又一点又一个注册界面出来了,哎,再一点又一个界面。你会发现根本停不下来
就像这样,好吧,你的boss马上就会疯了。
显然这种不太太合适,我们应该保证不论何时,点击注册的时候该注册窗体只有一个。
为了保证任何时刻都只有一个,显然不能随意创建,那么创建的过程就要由我们自己控制,且保证只创建一次
1.对外不能创建,那么我们的构造函数,是不是该标记为private
2.为了保证单例,必须有全局唯一的地方存储
3.所有的地方都不能创建实例了,那我们需要提供一个唯一的访问接口。
至于用属性还是用方法,感觉属性更好一点,没有什么太多的理由,访问起来更方便吧。
单例模式1:
为了保证上面的条件实现如下,
1.私有构造函数,保证了对外不能实例化
2.私有静态字段保证了全局唯一的存储
3.静态属性保证了对外的接口。
/// <summary> /// 问题:不能保证线程安全 /// </summary> public sealed class Singleton1 { private static Singleton1 _instatce1 = null; private Singleton1() { } public static Singleton1 Instance1 { get { if (_instatce1 == null) { _instatce1=new Singleton1(); } //_instatce1 = _instatce1 ?? new Singleton1(); return _instatce1; } } }
这就算ok了么,显然不是,从代码上看,我们并不能保证在多线程访问下,单例的唯一性。
当两个县城同时访问我们的Instance1属性时,同时判断_instatce1为null就可以同时进入代码,创建对象
单例模式2:
直接对静态字段初始化创建实例对象。
那么用静态字段直接初始化有什么作用呢。
1.在.net中静态构造函数的作用就是初始化类中的静态字段,静态构造函数不需访问修饰符,也不能带任何参数
2.静态构造函数是由.net运行时调用的,而不能在程序中调用,也就无法控制什么时候执行静态构造函数了。
3.由系统保证静态构造函数最多只被调用一次
4.如果没有写静态构造函数,而类中包含带有初始值设定的静态成员,那么编译器会自动生成默认的静态构造函数。
简而言之,我们对静态字段初始化实例,由系统保证只被调用一次,
/// <summary> /// 问题:初始化的时机不能保证,这里系统只保证在元数据中标记BeforeFieldInit,也就是在字段使用前初始化 /// </summary> public sealed class Singleton2 { private static readonly Singleton2 Instance2 = new Singleton2(); private Singleton2() { } public static Singleton2 Instance { get { return Instance2; } } }
我们可以看看IL代码。这个类确实有个 BeforeFieldInit 的标记
这里我们是没有自定义的静态构造函数的,系统会默认为我们提供一个默认的静态构造
正如上面分析,该单例方式最大的问题是,静态构造函数是由系统调用,而我们不能保证其调用时机。
对于beforefieldinit MSDN是这么解释的。
当一个类型声明显式静态构造函数时,实时 (JIT) 编译器会向该类型的每个静态方法和实例构造函数中添加一项检查,以确保之前已调用该静态构造函数。 访问任何静态成员或创建了类型实例时,都会触发静态初始化。 不过,如果您声明但未使用一个在初始化更改全局状态时非常重要的类型变量,则不会触发静态初始化。
如果已内联初始化所有静态数据,而未声明显式静态构造函数,则 Microsoft 中间语言 (MSIL) 编译器将 beforefieldinit标记和隐式静态构造函数(该构造函数会初始化静态数据)添加到 MSIL 类型定义中。 当 JIT 编译器遇到 beforefieldinit标记时,在大多数情况下,不会添加静态构造函数检查。 静态初始化将保证在访问任何静态字段之前的某个时刻发生,但不会在调用静态方法或实例构造函数前发生。 请注意,声明类型的变量后会随时发生静态初始化。
静态构造函数检查会降低性能。 通常,静态构造函数仅用于初始化静态字段,此时,只需确保静态初始化在第一次访问静态字段之前发生。 beforefieldinit 行为对这些类型和大多数其他类型是适当的。 仅在静态初始化影响全局状态且下列条件之一成立时不适当:
-
对全局状态的影响将耗费大量资源,如果不使用该类型则无需这样做。
-
无需访问该类型的任何静态字段,即可访问全局状态影响。
了解了上述信息,我们给你给出又一种单例方式,这种方式既保证了线程安全,又解决了延迟加载的问题。
但是该方式的使用是有条件的,(静态构造函数检查会降低性能。对全局状态的影响将耗费大量资源,如果不使用该类型则无需这样做)
也就是大对象,比较占用资源我们比较适合这么做。
单例模式4:
/// <summary> /// 静态构造函数方式 /// 增加了自定义的静态构造函数,那么也就不会在元数据中增加BeforeFieldInit个特性了 /// 当访问的时候才会调用静态构造函数进行初始化,那么也就是控制了初始化时机(延迟加载) /// </summary> public class Singleton4 { private static readonly Singleton4 Instance4 = new Singleton4(); private Singleton4() { } static Singleton4() { } public static Singleton4 Instance { get { return Instance4; } } public static Singleton4 GetInstance() { return Instance4; } }
单例模式3:双锁模式
volatile加双锁模式保证了线程安全的问题。应该也不存在延迟加载的问题。也是种不错的方式。只不过加锁也是会影响一部分效率
/// <summary> /// 双锁模式保证线程安全 /// Volatile 比同步更简单,只适合于控制对基本变量(整数、布尔变量等)的单个实例的访问。 /// 当一个变量被声明成 volatile,任何对该变量的写操作都会绕过高速缓存,直接写入主内存,而任何对 /// 该变量的读取也都绕过高速缓存,直接取自主内存。这表示所有线程在任何时候看到的 volatile 变 /// 量值都相同。 /// </summary> public sealed class Singleton3 { private static volatile Singleton3 instance3 = null; private static readonly object LockObj = new Object(); private Singleton3() { } public static Singleton3 Instance { get { if (instance3 == null) { lock (LockObj) { if (instance3 == null) instance3 = new Singleton3(); } } return instance3; } } }
对于volatile
volatile 关键字指示一个字段可以由多个同时执行的线程修改。 声明为 volatile 的字段不受编译器优化(假定由单个线程访问)的限制。 这样可以确保该字段在任何时间呈现的都是最新的值。
volatile总是与优化有关,编译器有一种技术叫做数据流分析,分析程序中的变量在哪里赋值、在哪里使用、在哪里失效,分析结果可以用于常量合并,常量传播等优化,进一步可以死代码消除。但有时这些优化不是程序所需要的,这时可以用volatile关键字禁止做这些优化,volatile的字面含义是易变的,它有下面的作用:
1 不会在两个操作之间把volatile变量缓存在寄存器中。在多任务、中断、甚至setjmp环境下,变量可能被其他的程序改变,编译器 自己无法知道,volatile就是告诉编译器这种情况。
2 不做常量合并、常量传播等优化,所以像下面的代码:
volatile int i = 1;
if (i > 0) ...
if的条件不会当作无条件真。
3 对volatile变量的读写不会被优化掉。如果你对一个变量赋值但后面没用到,编译器常常可以省略那个赋值操作,然而对Memory Mapped IO的处理是不能这样优化的。
volatile多用于多线程的环境,当一个变量定义为volatile时,读取这个变量的值时候每次都是从momery里面读取而不是从cache读。这样做是为了保证读取该变量的信息都是最新的,而无论其他线程如何更新这个变量。
单例模式5:
延迟加载机制。
在程序的生存期内,,特别是在这种方式创建或执行可能不发生使用延迟初始化延迟一种或大量占用资源的对象的创建、资源的任务的执行。
若要为延迟初始化准备,您创建 Lazy<T>实例。 Lazy<T> 对象的类型您创建的参数指定要延迟初始化对象的类型。 使用创建 Lazy<T> 对象的构造函数确定初始化的属性。
首次访问 Lazy<T>.Value 属性时会发生惰性初始化。
正如MSDN建议的一样,一般是占用大量资源的对象采用,估计这种方式也会降低部分效率吧。
/// <summary> /// 初始化 Lazy<T> 类的新实例。 发生延迟初始化时,使用指定的初始化函数,在需要时被调用以产生延迟初始化值的委托。 /// </summary> public class Singleton5 { // 因为构造函数是私有的,所以需要使用lambda private static readonly Lazy<Singleton5> Instance5 = new Lazy<Singleton5>(() => new Singleton5()); //new Lazy<Singleton>(() => new Singleton(), LazyThreadSafetyMode.ExecutionAndPublication); private Singleton5() { } public static Singleton5 Instance { get { return Instance5.Value; } } }
开始写的时候一直犹豫是提供方法还是属性呢,方法和属性不是一样的么
纠结半天又查了查,最后直接看IL,才确定没有区别的,不过属性调用更方便些,用属性比较划算,又能省几个括号。
其实不然,从代码的角度考虑并没有什么不同,但是有些场景却不一定。比如是多线程的情况下,单例需要被lock,那么用方法获取就很容易产生歧义了,
因为给人感觉就是这个对象是我通过方法“获取”到的,所以是独有的;但如果是静态的一个属性,就有一种公有的全局变量的感觉
如果单例是一个“值”,比如Configuration,那么用属性直观点;如果是一个工作对象,比如Worker、Helper、Client,那么用方法获取可能会合适些
那么net源码中有木有单例模式的应用呢,恩恩,前几天才看了过滤器的源码,怎么能忘了Providers方式提供过滤器呢。
public static class FilterProviders { // Methods static FilterProviders(); // Properties public static FilterProviderCollection Providers { get; private set; } } static FilterProviders() { Providers = new FilterProviderCollection(); Providers.Add(GlobalFilters.Filters); Providers.Add(new FilterAttributeFilterProvider()); Providers.Add(new ControllerInstanceFilterProvider()); }
恩恩,通过FilterProviders的静态属性Providers(只读)方式对外提供过滤器,在静态构造函数中FilterProviders进行了初始化。
这不就是个单例模式么。只不过跟我们的方式稍有不同而已。原来Providers 方式也就是种单例方式而已。