1.简介
上图中在点击菜单按钮后不断的弹出子窗体,显然这种方式是不合理的。此场景正是可以运用单例模式来解决的一种运用。
其核心本质就是让类的对象只有一个,使用到的地方还包括:线程池、缓存、对话框等。
单例模式其实很好理解,最核心的含义就是通过该设计模式来确保一个类只有一个实例,并对外提供一个全局访问的方式。
该模式最关键点就是“确保”,如何真正的确保一个类只有一个实例,理解单例的过程就是如何懂得实现真正意义上的“确保”。
2.单例模式的“三板斧”
2.1拒绝对外
设想下如果阿拉神灯允许你可以许一个愿望成真,这样的机会你会给别人吗?此场景同样可以隐喻到我们当下讲述的单例模式,
单例模式目前需求是一个类只能实例化一个对象,那么这样的机会你会给外人吗?此场景引入了实现单例模式的第一点——拒绝对外,
这里的拒绝对外说的是类创建对象的构造函数不允许外部进行调用。
如何在代码上实现呢?单例模式的一个关键点之一,构造函数私有化,这也印证了上述中说到的如果有件事情你只能做一次那么你肯定会留给自己。
public Singleton
{
private Singleton (){}
}
2.2.唯一对象的容器
通过私有构造函数对外关闭了实例化对象的入口,此时类的内部是实例化的唯一途径。那么现在需要为实例对象准备一个存储的容器来存储——声明静态变量。
public Singleton
{
private Singleton (){ }
private static Singleton uniqueObj=null;
}
2.3创建唯一对象并提供全局访问点
目前已经实现了构造函数私有化和对象的容器,那么现在需要实例化一个唯一的对象并提供外部访问的一种方式。
外部目前已经无法获取实例,那么唯一的方式就是提供一个静态的函数,外部直接通过类名来调用函数。在函数内部判断容器变量是否为空,
为空表示还没有创建实例,那么就通过私有构造函数来创建实例赋值到容器变量。在不为空的情况下说明唯一的对象已经存在,直接返回容器变量。
单例模式的“三板斧”最终代码如下:
如此一来就可以实现一个普通的单例模式,这里为什么说普通因为这个“三板斧”的组成是一个最常规并且还存在一些瑕疵的单例模式,
后续会讲具体的原因,我们先验证下这样的方式是否能保证类仅有一个实例。如图:
3.单例模式“三板斧”的瑕疵——多线程并发
上述讲的普通的单例模式如果使用了多线程获取对象实例,那么会导致调用方法后创建多个实例从而不能保证单个实例的需求。
我们以构造函数的特点来验证下这种情况,如图:
上图的验证中构造函数被执行多次,那么代表创建多个实例,从而证明多线程并发的时候无法保证对象的实例仅有一个。
4.如何解决多线程并发带来的问题
4.1.线程同步
使用C#关键字lock的特性实现线程同步。在并发的时候可能会有多个线程同时进入实例化对象的代码块中,
利用线程同步在访问实例化对象代码块时线程形成排队机制来确保不会同时创建多个实例。改进下代码来看看效果
如图:
此方式会有一个问题,实际上在第一次执行此方法时才需要同步。因为一旦设置好了实例对象,就没有必要进行同步。
而目前的做法是每次使用的时候都会执行同步,而同步次数过多会降低程序的性能,如果不考虑性能的消耗,那么也可以选择此方式。
4.2.静态变量
直接使用声明一个静态字段并对其进行实例化赋值,此做法是利用静态的原理来做到类仅有一个对象的方式。
我们可以回顾分析下静态成员的特点:
GC不会对静态成员进行资源回收并且会常驻于程序内存中;
静态成员使用静态构造函数初始化,静态构造函数由CLR执行并且只会执行一次从而保证了静态变量只会存储一个实例,另外该方式也解决了多线程并发带来的问题。
代码如下:
1 class Singleton 2 { 3 private Singleton() { } 4 private static Singleton uniqueObj = new Singleton(); 5 public static Singleton CreateInstance() 6 { 7 return uniqueObj; 8 } 9 }
4.2.1.“延迟实例化”
该方式是一种快速实现方式,代码上甚至可以简写:直接将静态变量改成公有的,外部直接访问这个公有的静态变量来实现单例模式。
该模式同样存在一个问题:此模式摒弃了“延迟实例化”。在使用lock的方式中,我们获取实例的方式是通过一个静态函数来获取的,
而目前的方式是直接使用静态字段。熟悉静态成员初始化的朋友应该清楚,静态成员初始化赋值的时候是当有代码对类型第一次访问的时候进行的。
我们试想一下,如果程序中存在一个单例模式,但是在使用某个功能时没有涉及到单例模式的使用,但是使用的这个功能访问到了单例模式的类型。
这就导致单例模式的对象没有使用的需求却被创建,这就导致了不必要的消耗。而lock方式中通过静态函数来获取,那么此方式只要真正有道单例模式的时候才会创建对应的实例对象。
此方式也有个名词叫做饿汉式,好比一桌菜没有上齐,饥饿的大汉就可以吃起来。
4.3.“双重检查加锁”
此方式是目前最为完善的方式,通过“双重检查加锁”的方式可以保证方法执行时只会在第一次的时候进行同步避免了多次同步的性能消耗,同时也能实现“延迟实例化”。
PS:另外通过程序进行线程调试就可以看到该方式只在第一次方法时才进行同步。
代码如下:
1 class Singleton 2 { 3 private Singleton() { Console.WriteLine("每创建一个对象调用一次构造函数"); } 4 private static Singleton uniqueObj = null; 5 private static object obj = new object(); 6 7 public static Singleton CreateInstance() 8 { 9 if (uniqueObj == null) 10 { 11 lock (obj) 12 { 13 if (uniqueObj == null) 14 { 15 uniqueObj = new Singleton(); 16 } 17 } 18 } 19 return uniqueObj; 20 } 21 }
5.Demo源码
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 using System; 2 using System.Collections; 3 using System.Collections.Generic; 4 using System.Data; 5 using System.Data.OleDb; 6 using System.Diagnostics; 7 using System.IO; 8 using System.Linq; 9 using System.Text; 10 using System.Text.RegularExpressions; 11 using System.Threading; 12 13 namespace MyDebug 14 { 15 16 class Singleton 17 { 18 #region MyRegion 19 private Singleton() { Console.WriteLine("每创建一个对象调用一次构造函数"); } 20 private static Singleton uniqueObj = null; 21 private static object obj = new object(); 22 23 public static Singleton CreateInstance() 24 { 25 if (uniqueObj == null) 26 { 27 lock (obj) 28 { 29 if (uniqueObj == null) 30 { 31 uniqueObj = new Singleton(); 32 } 33 } 34 } 35 return uniqueObj; 36 } 37 #endregion 38 39 #region 静态变量实例化(已注释) 40 //private Singleton() { } 41 //private static Singleton uniqueObj = new Singleton(); 42 //public static Singleton CreateInstance() 43 //{ 44 // return uniqueObj; 45 //} 46 #endregion 47 48 #region lock同步(已注释) 49 //private static Singleton uniqueObj = null; 50 //private static object obj = new object(); //lock资源对象 51 //public static Singleton CreateInstance() 52 //{ 53 // lock (obj) 54 // { 55 // if (uniqueObj == null) 56 // { 57 // uniqueObj = new Singleton(); 58 // } 59 // } 60 // return uniqueObj; 61 //} 62 #endregion 63 } 64 65 class Program 66 { 67 static void Main(string[] args) 68 { 69 //多线程 70 for (int i = 0; i < 5; i++) 71 { 72 Thread r1 = new Thread(() => 73 { 74 Singleton B = Singleton.CreateInstance(); 75 }); 76 r1.Start(); 77 } 78 79 Console.ReadKey(); 80 } 81 82 83 84 85 } 86 87 88 89 90 }