人生终极三问:你是谁?你从哪里来?你要到哪里去?
对于单例模式来说:是什么?为什么会有?怎么样实现?(Who,Why,How)
一、是什么
单例模式(Singleton Pattern),简单的说,就是一个类,只有一个实例。官方给的定义:确保一个类只有一个实例,并提供一个全局访问点。
二、为什么会有
一般而言:有需求才会有实现,有了具体的实现就能很好理解为什么了。
通常我们操作电脑,打开回收站,只会有一个操作窗口,同样的还有任务管理器。为什么会这样呢?如果不使用机制对窗口对象进行唯一化,将弹出多个窗口,如果这些窗口显示的内容完全一致,则是重复对象,浪费内存资源;如果这些窗口显示的内容不一致,则意味着在某一瞬间系统有多个状态,与实际不符,也会给用户带来误解,不知道哪一个才是真实的状态。因此有时确保系统中某个对象的唯一性即一个类只能有一个实例非常重要。这就需要用到单例模式了。
一般应用场景有:
- 需要生成唯一序列的环境
- 需要频繁实例化然后销毁的对象。
- 创建对象时耗时过多或者耗资源过多,但又经常用到的对象。
- 方便资源相互通信的环境
三、怎么样实现
我们从单例模式的概念入手,可以拆分为两部分:
- 确保一个类只有一个实例
- 提供一个访问它的全局访问点
如何保证一个类只有一个实例并且这个实例易于被访问呢?定义一个全局变量可以确保对象随时都可以被访问,但不能防止我们实例化多个对象。一个更好的解决办法是让类自身负责保存它的唯一实例。这个类可以保证没有其他实例被创建,并且它可以提供一个访问该实例的方法。这就是单例模式的模式动机。
- 某个类只能有一个实例
- 它必须自行创建这个实例
- 它必须自行向整个系统提供这个实例
- 单例模式的类只提供私有的构造函数
- 类定义中含有一个该类的静态私有对象
- 该类提供了一个静态的公有的函数用于创建或获取它本身的静态私有对象
通过上面的分析,实现代码:
/// <summary> /// 单例模式的实现 /// </summary> public class Singleton { // 定义一个静态变量来保存类的实例 private static Singleton uniqueInstance; // 定义私有构造函数,使外界不能创建该类实例 private Singleton() { } /// <summary> /// 定义公有方法提供一个全局访问点,同时你也可以定义公有属性来提供全局访问点 /// </summary> /// <returns></returns> public static Singleton GetInstance() { // 如果类的实例不存在则创建,否则直接返回 if (uniqueInstance == null) { uniqueInstance = new Singleton(); } return uniqueInstance; } }
上面的单例模式的实现在单线程下确实是完美的,然而在多线程的情况下会得到多个Singleton实例,因为在两个线程同时运行GetInstance方法时,此时两个线程判断(uniqueInstance ==null)这个条件时都返回真,此时两个线程就都会创建Singleton的实例,这样就违背了我们单例模式初衷了,既然上面的实现会运行多个线程执行,那我们对于多线程的解决方案自然就是使GetInstance方法在同一时间只运行一个线程运行就好了。
通过上面的分析,实现代码:
/// <summary> /// 单例模式的实现 /// </summary> public class Singleton { // 定义一个静态变量来保存类的实例 private static Singleton uniqueInstance; // 定义一个标识确保线程同步 private static readonly object locker = new object(); // 定义私有构造函数,使外界不能创建该类实例 private Singleton() { } /// <summary> /// 定义公有方法提供一个全局访问点,同时你也可以定义公有属性来提供全局访问点 /// </summary> /// <returns></returns> public static Singleton GetInstance() { // 当第一个线程运行到这里时,此时会对locker对象 "加锁", // 当第二个线程运行该方法时,首先检测到locker对象为"加锁"状态,该线程就会挂起等待第一个线程解锁 // lock语句运行完之后(即线程运行完之后)会对该对象"解锁" lock (locker) { // 如果类的实例不存在则创建,否则直接返回 if (uniqueInstance == null) { uniqueInstance = new Singleton(); } } return uniqueInstance; } }
上面这种解决方案确实可以解决多线程的问题,但是上面代码对于每个线程都会对线程辅助对象locker加锁之后再判断实例是否存在,对于这个操作完全没有必要的,因为当第一个线程创建了该类的实例之后,后面的线程此时只需要直接判断(uniqueInstance==null)为假,此时完全没必要对线程辅助对象加锁之后再去判断,所以上面的实现方式增加了额外的开销,损失了性能,为了改进上面实现方式的缺陷,我们只需要在lock语句前面加一句(uniqueInstance==null)的判断就可以避免锁所增加的额外开销,这种实现方式我们就叫它 “双重锁定”。
通过上面的分析,实现代码:
/// <summary> /// 单例模式的实现 /// </summary> public class Singleton { // 定义一个静态变量来保存类的实例 private static Singleton uniqueInstance; // 定义一个标识确保线程同步 private static readonly object locker = new object(); // 定义私有构造函数,使外界不能创建该类实例 private Singleton() { } /// <summary> /// 定义公有方法提供一个全局访问点,同时你也可以定义公有属性来提供全局访问点 /// </summary> /// <returns></returns> public static Singleton GetInstance() { // 当第一个线程运行到这里时,此时会对locker对象 "加锁", // 当第二个线程运行该方法时,首先检测到locker对象为"加锁"状态,该线程就会挂起等待第一个线程解锁 // lock语句运行完之后(即线程运行完之后)会对该对象"解锁" // 双重锁定只需要一句判断就可以了 if (uniqueInstance == null) { lock (locker) { // 如果类的实例不存在则创建,否则直接返回 if (uniqueInstance == null) { uniqueInstance = new Singleton(); } } } return uniqueInstance; } }
参考文献:
1.《C#设计模式(1)--单例模式》http://www.cnblogs.com/zhili/p/SingletonPatterm.html
2.《单例模式(Singleton)应用场景和优缺点》https://blog.csdn.net/Mr_PH/article/details/77898243