单例模式(Singleton)
一、概要
单例模式,提倡简约而不简单,透漏一种简捷美。它保证一个类仅有一个实例,并提供一个访问它的全局访问点。要点有三个;一是某个类只能有一个实例;二是它必须自行创建这个实例;三是它必须自行向整个系统提供这个实例。
二、生活中的例子
1、在中国,一个男人只能有一个合法妻子
2、世界上只能有一个中国
3、windows只能有一个任务管理器
等等
三、实现思路
一个类能返回对象一个引用(永远是同一个)和一个获得该实例的方法(必须是静态方法,通常使用getInstance这个名称);当我们调用这个方法时,如果类持有的引用不为空就返回这个引用,如果类保持的引用为空就创建该类的实例并将实例的引用赋予该类保持的引用;同时我们还将该类的构造函数定义为私有方法,这样其他处的代码就无法通过调用该类的构造函数来实例化该类的对象,只有通过该类提供的静态方法来得到该类的唯一实例。
四、类图和对象图
1、类图(根据上述实现思路画得类图)
2、对象图,在下面的对象图中,有一个"单例对象",而"客户1"、"客户2" 和"客户3"是单例对象的三个客户对象。可以看到,所有的客户对象共享一个单例对象。而且从单例对象到自身的连接线可以看出,单例对象持有对自己的引用。
五、注意点
单例模式在多线程的应用场合下必须小心使用。如果当唯一实例尚未创建时,有两个线程同时调用创建方法,那么它们同时没有检测到唯一实例的存在,从而同时各自创建了一个实例,这样就有两个实例被构造出来,从而违反了单例模式中实例唯一的原则。解决这个问题的办法是为指示类是否已经实例化的变量提供一个互斥锁(虽然这样会降低效率)。
六、构造方式
语言:C#
方案1、基本模型
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace 单例模式基本模型 { class Program { //客户端 static void Main(string[] args) { Singleton Instance1 = Singleton.GetInstance(); Singleton Instance2 = Singleton.GetInstance(); if (Instance1 == Instance2) { Console.WriteLine("两个实例是一模一样的实例。"); } } } class Singleton { private static Singleton instance; //构造方法采用private,外界便无法用new创建该类的实例 private Singleton() { } //该方法提供一个获得该类实例的全局访问点,是唯一的 public static Singleton GetInstance() { //如果实例不存在,则返回一个新实例,否则返回原实例。 if (instance == null) { instance = new Singleton(); } return instance; } } }
运行结果:两个实例是一模一样的实例。
优点:直到对象要求产生一个实例才执行实例化,即要在第一次引用时,才会自己实例化,这种构造方式成为懒汉式。
缺点:线程不安全,多线程情况下,多个线程同时访问Singleton,调用GetInstance()方法,同时判断instance==null,得到真值,导致创建多个实例,这不符合单例模式的基本原则。
为了克服上述缺点,我们来看如何改进
方案2、线程安全的构造方式
class Singleton { private static Singleton instance=null; //创建一个静态只读的进程辅助对象 private static readonly object ProgressLock = new object(); //构造方法采用private,外界便无法用new创建该类的实例 private Singleton() { } //该方法提供一个获得该类实例的全局访问点,是唯一的 public static Singleton GetInstance() { lock (ProgressLock) { if (instance == null) { instance = new Singleton(); } } return instance; } }
客户端代码同上
运行结果:两个实例是一模一样的实例。
这种构造方式,我们先创建了一个进程辅助对象,线程在进入时先对辅助对象加锁然后再检测对象是否被创建,这样可以确保只有一个实例被创建,因为在同一个时刻加了锁的那部分程序只有一个线程可以进入。
优点:进程安全
缺点:每次调用GetInstance()方法都需要lock,肯定会损失性能。
接下来,我们再进行更深一步的优化。
方案3、双重锁定
class Singleton { private static Singleton instance=null; //创建一个静态只读的进程辅助对象 private static readonly object ProgressLock = new object(); //构造方法采用private,外界便无法用new创建该类的实例 private Singleton() { } //该方法提供一个获得该类实例的全局访问点,是唯一的 public static Singleton GetInstance() { if (instance == null) { lock (ProgressLock) { if (instance == null) { instance = new Singleton(); } } } return instance; } }
客户端代码同上
运行结果:两个实例是一模一样的实例。
有人会问:为什么加锁内外用了两次instance== null的判断?
答:举例说明,当instance == null为真时,有多个线程同时通过第一层判断,由于lock,只有一个进入,另一个在外面等候。这个时候,就会出现,前一个创建了实例,出锁后,后一个线程又进锁创建了一个实例,这就出现了多个实例的情况。
优点:这种构造方式只有在实例未被创建的时候才加锁,避免了每次调用GetInstance()方法都加锁损失性能的问题。
缺点:有争议,对于后面的方式,它仍不够完美。
实际应用中,我们更多地采用下面的方式
方案4、静态初始化方式
//sealed关键字防止派生 public sealed class Singleton { //在第一次引用类的成员时创建实例,公共语言运行库负责处理变量的初始化 private static readonly Singleton instance=new Singleton(); //构造方法采用private,外界便无法用new创建该类的实例 private Singleton() { } public static Singleton GetInstance() { return instance; } }
客户端同上
运行结果:两个实例是一模一样的实例。
这种方式是在自己被加载时就将自己实例化,成为饿汉式。
变量标记为readonly,这意味着只能在静态初始化期间(此处显示的示例)或在类构造函数中分配变量。该类标记为 sealed 以阻止发生派生,而派生可能会增加实例
优点:该实现与前面的示例类似,不同之处在于它依赖公共语言运行库来初始化变量。它仍然可以用来解决Singleton模式试图解决的两个基本问题:全局访问和实例化控制。
缺点:由于有.NetFramework 进行初始化,所以我们对实例化机制的控制权较少,没办法和其他实现一样实现延迟初始化。在上面三种形式中,您能够在实例化之前使用非默认的构造函数或执行其他任务。
方案5、完全延迟加载实例化
public sealed class Singleton { private Singleton() { } public static Singleton GetInstance() { return Nested.instance; } class Nested { static Nested() { } internal static readonly Singleton instance = new Singleton(); } }
客户端同上
运行结果:两个实例是一模一样的实例。
在这里,含有静态成员的Nested类将触发实例化。
这就意味着这个实例化过程是完全延迟的,比前一个版本有着更好的性能。
七、总结
单例模式是一个简约而实用的设计模式,上述五种实现方式中,方案4(静态初始化方式)是类一加载就实例化的对象,所以提前占用系统资源,然后,前三种,因为会面临多线程访问安全的问题,需要做双重锁定这样的处理才可以保证安全,但能够在实例化之前使用非默认的构造函数或执行其他任务。是否可以延迟初始化也是两者的显著区别。所以到底实用哪一种方式,取决于实际需求,个人认为,静态初始化方式已经能满足我们需要,如果需要延迟初始化,不妨采用方案5.