单例模式(也叫单件模式)的作用就是保证在整个应用程序的生命周期中,任何一个时刻,单例类的实例都只存在一个(当然也可以不存在)。
下面来看单例模式的结构图:
从上面的类图中可以看出单例模式的特点:
- 在单例类中有一个构造函数 Singleton ,但是这个构造函数却是私有的
- 公开了一个 GetInstance()方法
通过上面的类图不难看出单例模式的特点,从而也可以给出单例模式的定义:
单例模式保证一个类仅有一个实例,同时这个类还必须提供一个访问该类的全局访问点。
1.最基本的单例:
namespace Singleton { public class Singleton { //定义一个私有的静态全局变量来保存该类的唯一实例 private static Singleton singleton; /// <summary> /// 构造函数必须是私有的 /// 这样在外部便无法使用 new 来创建该类的实例 /// </summary> private Singleton() { } /// <summary> /// 定义一个全局访问点 /// 设置为静态方法 /// 则在类的外部便无需实例化就可以调用该方法 /// </summary> /// <returns></returns> public static Singleton GetInstance() { //这里可以保证只实例化一次 //即在第一次调用时实例化 //以后调用便不会再实例化 if (singleton == null) { singleton = new Singleton(); } return singleton; } } }
2.线程安全单例
namespace Singleton { public class Singleton { //定义一个私有的静态全局变量来保存该类的唯一实例 private static Singleton singleton; //定义一个只读静态对象 //且这个对象是在程序运行时创建的 private static readonly object syncObject = new object(); /// <summary> /// 构造函数必须是私有的 /// 这样在外部便无法使用 new 来创建该类的实例 /// </summary> private Singleton() { } /// <summary> /// 定义一个全局访问点 /// 设置为静态方法 /// 则在类的外部便无需实例化就可以调用该方法 /// </summary> /// <returns></returns> public static Singleton GetInstance() { //这里可以保证只实例化一次 //即在第一次调用时实例化 //以后调用便不会再实例化 //第一重 singleton == null if (singleton == null) { lock (syncObject) { //第二重 singleton == null if (singleton == null) { singleton = new Singleton(); } } } return singleton; } } }
- 为何要使用双重检查锁定呢?
考虑这样一种情况,就是有两个线程同时到达,即同时调用 GetInstance(),
此时由于 singleton == null ,所以很明显,两个线程都可以通过第一重的 singleton == null ,
进入第一重 if 语句后,由于存在锁机制,所以会有一个线程进入 lock 语句并进入第二重 singleton == null ,
而另外的一个线程则会在 lock 语句的外面等待。
而当第一个线程执行完 new Singleton()语句后,便会退出锁定区域,此时,第二个线程便可以进入 lock 语句块,
此时,如果没有第二重 singleton == null 的话,那么第二个线程还是可以调用 new Singleton()语句,
这样第二个线程也会创建一个 Singleton 实例,这样也还是违背了单例模式的初衷的,
所以这里必须要使用双重检查锁定。
- 其实在没有第一重 singleton == null 的情况下,也是可以实现单例模式的,那么为什么需要第一重 singleton == null 呢?
这里就涉及一个性能问题了,因为对于单例模式的话,new Singleton()只需要执行一次就 OK 了,
而如果没有第一重 singleton == null 的话,每一次有线程进入 GetInstance()时,均会执行锁定操作来实现线程同步,
这是非常耗费性能的,而如果我加上第一重 singleton == null 的话,
那么就只有在第一次,也就是 singleton ==null 成立时的情况下执行一次锁定以实现线程同步,
而以后的话,便只要直接返回 Singleton 实例就 OK 了而根本无需再进入 lock 语句块了,这样就可以解决由线程同步带来的性能问题了。
好,关于多线程下单例模式的实现的介绍就到这里了,但是,关于单例模式的介绍还没完。
下面将要介绍的是懒汉式单例和饿汉式单例
1.懒汉式单例
何为懒汉式单例呢,可以这样理解,单例模式呢,其在整个应用程序的生命周期中只存在一个实例,
懒汉式呢,就是这个单例类的这个唯一实例是在第一次使用 GetInstance()时实例化的,
如果您不调用 GetInstance()的话,这个实例是不会存在的,即为 null
形象点说呢,就是你不去动它的话,它自己是不会实例化的,所以可以称之为懒汉。
其实呢,我前面在介绍单例模式的这几个 Demo 中都是使用的懒汉式单例,
从前面的这个 GetInstance()中可以看出这个单例类的唯一实例是在第一次调用 GetInstance()时实例化的,
所以此为懒汉式单例。
2.饿汉式单例
上面介绍了饿汉式单例,到这里来理解懒汉式单例的话,就容易多了,懒汉式单例由于人懒,
所以其自己是不会主动实例化单例类的唯一实例的,而饿汉式的话,则刚好相反,
其由于肚子饿了,所以到处找东西吃,人也变得主动了很多,所以根本就不需要别人来催他实例化单例类的为一实例,
其自己就会主动实例化单例类的这个唯一类。
在 C# 中,可以用特殊的方式实现饿汉式单例,即使用静态初始化来完成饿汉式单例模式
下面就来看一看饿汉式单例类
namespace Singleton { public sealed class Singleton { private static readonly Singleton singleton = new Singleton(); private Singleton() { } public static Singleton GetInstance() { return singleton; } } }
要先在这里提一下的是使用静态初始化的话,无需显示地编写线程安全代码,
C# 与 CLR 会自动解决前面提到的懒汉式单例类时出现的多线程同步问题。
上面的饿汉式单例类中可以看到,当整个类被加载的时候,就会自行初始化 singleton 这个静态只读变量。
而非在第一次调用 GetInstance()时再来实例化单例类的唯一实例,所以这就是一种饿汉式的单例类。
好,到这里,就真正的把单例模式介绍完了,在此呢再总结一下单例类需要注意的几点:
一、单例模式是用来实现在整个程序中只有一个实例的。
二、单例类的构造函数必须为私有,同时单例类必须提供一个全局访问点。
三、单例模式在多线程下的同步问题和性能问题的解决。
四、懒汉式和饿汉式单例类。
五、C# 中使用静态初始化实现饿汉式单例类。