对所学,所做做一个总结,也算是自己的一个另类总结吧!从单例开始,从遇到的问题和有感触的问题开始,杂乱无章,写入随笔
--by ling
此处从什么是单例,为什么要用单例,实现单例模式的整体思路三个方面来说明。最后讨论一下目前C#出现的各种单例模式的实现方式。
1、什么是单例
单例模式(Singleton),也叫单子模式,是一种常见的软件设计模式。在运用这个模式时,单例对象的类在程序的生命周期内必须保证只有一个实例存在。
单例模式结构图如下:
2、为什么要用单例模式
许多时候,整个系统只需要拥有一个全局对象,这样有利于我们协调系统的整体行为。比如:在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据,我们就可以由一个单例对象统一读取,然后服务器京城中的其他对象再通过这个单例对象获取这些配置信息。这样做即简化了在复杂环境下的配置管理,也减少了线程IO操作,提高了程序的执行效率。再比如:在某个程序中,某个或者某些数据一旦程序启动,就不会发生变化,而且整个软件的很多方法或者实例都会用到它(们),那么,为了让程序更简捷也更有效率,我们可以通过一个单例对象统一保存,加载后其他对象通过这个单例在内存中调用或者访问这个单例。等等......
3、实现单例模式的整体思路
一个类永远返回对象的同一个引用;该类只有一个唯一的获得该实例的方法,该方法只能是静态方法。当我们调用这个方法时,如果类持有的引用不为空就返回这个引用,如果这个类持有的引用为空,则创建该类的实例,并将实例的引用赋予该类保持的引用。同时我们还需要将该类的构造方法(构造函数)定义为私有方法,使得他处的代码无法通过调用该类的构造方法来实例化该类的对象,只能通过公布出来的获得该类的唯一静态方法获取该类的唯一实例。
4、单例的实现方式
通过漫长的编程积累,单例模式也根据实际的项目需求发生了演变:从一开始的非线程安全到线程安全,从不需要考虑多线程性能到需要考虑性能,从预加载到延迟加载等多方面的演变。
通过上述对单例模式的介绍,我们可以总结单例模式的一下特性:
1、单例对象类,只能有一个唯一实例,因此该类不能被代码的其他引用无限制的实例化,有且只有一个地方实例化单例对象,且实例化以后不再接受其他的实例化;不能被继承,如果该单例对象的类被继承,则其子类对象就可以无限实例化,所以单例对象类,不能被继承,在c#中可以通过sealed类或者static类来标记该类不能被继承。由于静态类中只能存在静态成员,因此与单例模式的设计思路并不符合,所以单例对象所在的类应该是一个密封类(sealed类)
此处,扩展一下静态类和非静态类的区别,以便于理解为什么单例对象的类需要是sealed类而非静态类,亦或是其他类。如果熟悉C#的类修饰符的读者,可以忽略此段。首先,我们了解下C#的修饰符,分为两大类:访问修饰符(4个5种访问限制)和声明修饰符(8个)。
[1] 访问修饰符
public:访问不受限制,可以在类内部和任何类外的代码(类外的代码包括:程序集内部所有的代码(包含子类等)和引用该程序集的所有代码)中访问。
inernal:访问域受限于当前程序集(包括程序集所有程序和该程序集内的子类,当然程序集以外的类也不能由该类派生),程序集以外的代码不可见。
protected:访问域受限于该类内部或者是该类的派生类内部,其他代码不可见。
protected internal:访问域受限于该程序集内或者该类的派生类可见,其他代码不可见。
private:私有访问域,受限于该类内部,其他代码不可见。
实际上只有四个:public internal protected private 四个,但由于可以组合,多出一种访问方式:protected internal
[2] 声明修饰符
partial:在同一个程序集中定义分部类或结构(一般常见于声明组合类)。
static:声明属于类型本身而不是特定对象的成员,如果用于声明类,则该类的本质为密封类,且该类中只能有静态成员,不能包含构造函数,不能被实例化。
abstract:声明抽象类或抽象方法。如果是抽象类,只能作为其他类的基类存在。如果是抽象方法,则只能声明方法而不实现,需要在其派生类中实现。
sealed:修饰类时,该类为密封类不能被继承。修饰属性或方法时,必须始终与override组合使用,声明后的属性或方法不能在其派生类中再度被重写 。
virtural:用于修饰方法、属性、索引器或者事件声明,并且允许在派生类中重写这些对象。不能与static、abstract、private和override修饰符一起使用。除了声明和调用语法不同外,虚拟属性的行为与抽象方法一样。
override:提供从基类继承的成员的新实现。
new:作为修饰符时,可以显示隐藏从基类继承的成员。这就意味着,从该类派生的派生成员将替换基类版本的成员,使基类版本的成员被隐藏。当然,new关键字还可以作为运算符和约束存在。作为运算符时,用于创建对象和调用构造函数(即实例化对象)。作为约束时,约束指定泛型类中声明的任何类型参数都必须有公共的无惨构造函数。当泛型类创建类型的新实例时,将此榆树用于类型参数。如
1 class ItemFactory<T> where T : new() 2 { 3 public T GetNewItem() 4 { 5 return new T(); 6 } 7 } 8 9 //当与其他约束一起使用时,new()约束应该放在最后 10 public class ItemFactory<T> 11 where T : IComparable, new() 12 { 13 }
extern:用于声明在外部实现的方法。常见于使用interop服务调入非托管代码时,与dllimport特性一起使用。在这种情况下,还必须将方法声明为static,如下:
[DllImport("avifil32.dll")] private static extern void AVIFileInit();
介绍了这么多,终于要介绍静态类和非静态类的区别了。其实介绍这些是为不熟悉这些修饰符的人准备的,当然也是为了让自己有更深入的理解。
静态类与非静态类的重要区别就在于,静态类不能实例化,也就是说,不能使用new作为运算符创建该类的实例。它的意义在于,首先,它防止程序员写代码来实例化静态类;其次,它防止该类的内部声明中没有任何非静态的字段或方法,即该静态类中只能存在静态成员;第三,静态类不能包含构造函数,其本质是密封类。
因此,也就可以理解,为什么单例不能是static类了,因为单例需要有构造方法,只是单例的构造方法需要时私有的。同时,静态类中不能存在非静态的成员,但,单例对象的类中,允许非静态对象,或者说本就是为了提供这些对象。所以,静态类,一般用于声明一些常量性质的共享数据,这类数据,在允许以前就会被编译和实例化,在整个程序的声明周期中存在。这和单例对象的特性有些相似,但,单例解决了非静态成员的问题,使用场景不一样。
下面正式介绍单例模式在C#中实现的几种方式,并详细说明各种方式的优缺点:
[1] 非线程安全的单例模式(现实中,我们不得不考虑线程安全,所以这种模式,实际上不实用)
1 /// <summary> 2 /// 非线程安全单例 3 /// </summary> 4 public sealed class Singleton 5 { 6 private static Singleton _instance; 7 8 private Singleton() 9 { 10 } 11 12 public static Singleton Instance 13 { 14 get 15 { 16 if (_instance == null) 17 { 18 _instance = new Singleton(); 19 } 20 21 return _instance; 22 } 23 } 24 }
这种非线程安全的单例对象,适用于单线程环境。同时,它是一个延迟加载的单例,只有在用到它的时候才会去实例化对象,并且实例化一次以后,一直到程序的生命周期结束,都会存在。因此,如果是单线程应用中,如果需要用到单例模式,可以考虑这种声明方式。当然,对于多线程应用来说,它是不安全的,谁也不会知道会不会同时有两个或者以上的线程同时发起访问,这种情况下,就会实例化多个单例对象。所以,在多线程模式下,我们很容易就想到用加锁的方式对其进行限制。
[2] 线程安全的、加锁的单例
/// <summary> /// 线程程安全单例(加锁单例) /// </summary> public sealed class Singleton { private static Singleton _instance; //锁对象 private static object _objLock = new object(); private Singleton() { } public static Singleton Instance { get { lock (_objLock) { if (_instance == null) { _instance = new Singleton(); } } return _instance; } } } }
这种方式的单例,同样是延迟加载的单例。同时,因为加了对象锁,可以确保当一个线程位于代码临界区时,另一个线程不能进入临界区,直到该对象被释放。所以,它是线程安全的。当然,也正是因为每次访问的时候都要进行锁定,所以,增加了系统的额外开销,影响了系统的性能瓶颈。因此,我们需要考虑是否能够优化这种单例模式,即让单例线程安全,又能减小系统开销。自然而然的,我们会想到,减少锁定的次数,并不是每次访问都先进行锁定,而是先判断是否有对象。
[3] 双重判断、加锁单例
/// <summary> /// 线程程安全单例(加锁单例) /// 双重检测单例(Double-Checked Locking) /// </summary> public sealed class Singleton { private static Singleton _instance; //锁对象 private static object _objLock = new object(); private Singleton() { } public static Singleton Instance { get { if (_instance == null) { lock (_objLock) { if (_instance == null) { _instance = new Singleton(); } } } return _instance; } } } }
通过双重检测,确实减少了对锁的访问,但还是有锁,同样会影响性能。那么我们能不能,即让线程安全,同时又无锁呢?答案是肯定的,C#提供了嵌套类,我们可以利用这种语言特性对单例模式进行改造,让它即线程安全,又无锁,达到安全高效的目的。
[4] 嵌套类实现单例
/// <summary> /// 通过嵌套类实现单例 /// </summary> public sealed class Singleton { private static Singleton _instance; private Singleton() { } public static Singleton Instance { get { return SingletonChiled.Instance; } } /// <summary> /// 嵌套类 /// </summary> class SingletonChiled { static SingletonChiled() { } internal static readonly Singleton Instance = new Singleton(); } } } //根据c#的匿名内部内的特性,我们还可以改写为以下代码和.net 4.0以上可以使用Lazy<T> 延时加载类来实现,更为简洁和方便 /// <summary> /// 使用Lazy<T>延时加载类进行声明单例 /// 要求.net framework 在4.0以上 /// </summary> public sealed class Singleton { private static readonly Lazy<Singleton> lazy = new Lazy<Singleton>(() => new Singleton()); private Singleton() { } public static Singleton Instance { get { return lazy.Value; } } }
最后一种,也是比较简单的一种单例实现方式,同样是线程安全,同样无锁,只是非延迟加载,其实和第四种差不多,区别在于:第四种是使用到的时候才会声明,而这种,则编译的时候就声明好
[5] 非延迟加载的、线程安全的、无锁的单例
1 /// <summary> 2 /// 非延迟加载的、线程安全的、无锁的单例 3 /// </summary> 4 public sealed class Singleton 5 { 6 private static Singleton _instance=new Singleton(); 7 8 private Singleton() 9 { 10 } 11 12 public static Singleton Instance 13 { 14 get 15 { 16 return _instance; 17 } 18 } 19 }
仅此以记录单例模式,个人对推荐使用第四种和第五种,具体根据项目需求决定