单例模式(Singleton pattern)
一、什么是单例模式
单例模式(Singleton)是几个创建模式中最对立的一个,它的主要特点不是根据用户程序调用生成一个新的实例,而是控制某个类型的实例唯一性,通过下图我们知道它包含的角色只有一个,就是Singleton,它拥有一个私有构造函数,这确保用户无法通过new直接实例它。除此之外,该模式中包含一个静态私有成员变量instance与静态公有方法Instance()。Instance()方法负责检验并实例化自己,然后存储在静态成员变量中,以确保只有一个实例被创建。
二、单例模式的特点
- 有一个私有的无参构造函数,这可以防止其他类实例化它,而且单例类也不应该被继承,如果单例类允许继承那么每个子类都可以创建实例,这就违背了Singleton模式“唯一实例”的初衷。
- 单例类被定义为sealed,就像前面提到的该类不应该被继承,所以为了保险起见可以把该类定义成不允许派生,但没有要求一定要这样定义。
- 一个静态的变量用来保存单实例的引用。
- 一个公有的静态方法用来获取单实例的引用,如果实例为null即创建一个。
三、单例模式的优点
- 实例控制:单例模式防止其它对象对自己的实例化,确保所有的对象都访问一个实例。
- 伸缩性:因为由类自己来控制实例化进程,类就在改变实例化进程上有相应的伸缩性。
四、单例模式的缺点
- 系统开销。虽然这个系统开销看起来很小,但是每次引用这个类实例的时候都要进行实例是否存在的检查。这个问题可以通过静态实例来解决。
- 开发混淆。当使用一个单例模式的对象的时候(特别是定义在类库中的),开发人员必须要记住不能使用new关键字来实例化对象。因为开发者看不到在类库中的源代码,所以当他们发现不能实例化一个类的时候会很惊讶。
- 对象生命周期。单例模式没有提出对象的销毁。在提供内存管理的开发语言(比如,基于.NetFramework的语言)中,只有单例模式对象自己才能将对象实例销毁,因为只有它拥有对实例的引用。在各种开发语言中,比如C++,其它类可以销毁对象实例,但是这么做将导致单例类内部的指针指向不明。
五、单例模式的适用场景
- 使用Singleton模式有一个必要条件:在一个系统要求一个类只有一个实例时才应当使用单例模式。反之,如果一个类可以有几个实例共存,就不要使用单例模式。
- 不要使用单例模式存取全局变量。这违背了单例模式的用意,最好放到对应类的静态成员中。
- 不要将数据库连接做成单例,因为一个系统可能会与数据库有多个连接,并且在有连接池的情况下,应当尽可能及时释放连接。Singleton模式由于使用静态成员存储类实例,所以可能会造成资源无法及时释放,带来问题。
六、单例模式的实例
实例1:可以考虑实现单例模式的地方还有很多,例如:
1) 对于计算机的外部资源打印机的情况,因只有一个Printer Spooler,为避免两个打印作业同时输出到打印机中,可考虑用单例模式实现。
2) Window的回收站在整个系统中只有唯一的一个实例,而且回收站自行提供自己的实例,回收站也是单例模式的应用
单例类(打印服务)
1 public class PrintSploor
2 {
3 //定义一个【私有的】实例方法,预防通过PrintSploor s=new PrintSploor(); 这样就做不到单例
4 private PrintSploor() { }
5
6 //创建一个私有的实例对象,保证是唯一的
7 private static PrintSploor instance = null;
8
9 //提供一个获取实例的公共方法
10 public static PrintSploor Instance
11 {
12 get
13 {
14 if (null == instance)
15 {
16 instance = new PrintSploor();
17 }
18
19 return instance;
20 }
21 }
22 }
客户端代码
1 public class Program
2 {
3 static void Main(string[] args)
4 {
5 PrintSploor printSploor1 = PrintSploor.Instance;
6 PrintSploor printSploor2 = PrintSploor.Instance;
7
8 if (printSploor1 == printSploor2)
9 {
10 Console.WriteLine("说明是同一对象,是单例。");
11 }
12 else
13 {
14 Console.WriteLine("说明不是同一对象,不是单例。");
15 }
16
17 Console.Read();
18 }
19 }
以上代码完成了在单线程程序中的单例模式,如果我们现在的程序是多线程的话,还是用这个单例类的话,就有可能会出现PrintSploor类的多个程序。那我们又该怎么避免呢?请看以下几种代码的实现:
方法一:
首先我们创建了一个静态只读的进程辅助对象,由于lock是确保当一个线程位于代码的临界区时,另一个线程不能进入临界区(同步操作)。如果其他线程试图进入锁定的代码,则它将一直等待,直到该对象被释放。从而确保在多线程下不会创建多个对象实例了。
1 public class PrintSploor
2 {
3 //定义一个【私有的】实例方法,预防通过PrintSploor s=new PrintSploor(); 这样就做不到单例
4 private PrintSploor() { }
5
6 //创建一个私有的实例对象,保证是唯一的
7 private static PrintSploor instance = null;
8
9 private static object syncObject = new object();
10
11 //提供一个获取实例的公共方法
12 public static PrintSploor Instance
13 {
14 get
15 {
16
17 lock (syncObject)
18 {
19 if (null == instance)
20 {
21 instance = new PrintSploor();
22 }
23 }
24
25 return instance;
26 }
27 }
28 }
方法二:
前面讲到的线程安全的实现方式的问题是要进行同步操作,那么我们是否可以降低通过操作的次数呢?其实我们只需在同步操作之前,添加判断该实例是否为null就可以降低通过操作的次数了,这样是经典的Double-Checked Locking方法。
1 public class PrintSploor
2 {
3 //定义一个【私有的】实例方法,预防通过PrintSploor s=new PrintSploor(); 这样就做不到单例
4 private PrintSploor() { }
5
6 //创建一个私有的实例对象,保证是唯一的
7 private static PrintSploor instance = null;
8
9 private static object syncObject = new object();
10
11 //提供一个获取实例的公共方法
12 public static PrintSploor Instance
13 {
14 get
15 {
16 if (null == instance)
17 {
18 lock (syncObject)
19 {
20 if (null == instance)
21 {
22 instance = new PrintSploor();
23 }
24 }
25 }
26
27 return instance;
28 }
29 }
30 }
方法三:
通过在类中创建instance静态字段的时候直接new出一个实例来,然后通过Instance属性来实现唯一获取该类实例的返回点,从而实现PrintSploor只有一个实例。达到单例模式的效果
1 public class PrintSploor
2 {
3 //定义一个【私有的】实例方法,预防通过PrintSploor s=new PrintSploor(); 这样就做不到单例
4 private PrintSploor() { }
5
6 //创建一个私有的实例对象,保证是唯一的
7 private static PrintSploor instance = new PrintSploor();
8
9 private static object syncObject = new object();
10
11 //提供一个获取实例的公共方法
12 public static PrintSploor Instance
13 {
14 get
15 {
16 return instance;
17 }
18 }
19 }