首先来明确一个问题,那就是在某些情况下,有些对象,我们只需要一个就可以了,比如,一台计算机上可以连好几个打印机,但是这个计算机上的打印程序只能有一个,这里就可以通过单例模式来避免两个打印作业同时输出到打印机中,即在整个的打印过程中我只有一个打印程序的实例。
简单说来,单例模式的作用就是保证在整个应用程序的生命周期中,任何一个时刻,单例类的实例都只存在一个(当然也可以不存在)。
下面来看单例模式的结构图
从上面的类图中可以看出,在单例类中有一个构造函数 Singleton ,但是这个构造函数却是私有的(前面是“ - ”符号),然后在里面还公开了一个 GetInstance()方法。通过上面的类图不难看出单例模式的特点,从而也可以给出单例模式的定义:单例模式保证一个类仅有一个实例,同时这个类还必须提供一个访问该类的全局访问点。
下边是单例模式的运行代码:
<span style="font-family:KaiTi_GB2312;font-size:18px;"><strong>namespace 单例模式
{
class Program
{
static void Main(string[] args)
{
Singleton s1 = Singleton.GetInstance();
Singleton s2 = Singleton.GetInstance();
if (s1 == s2)
{
Console.WriteLine("两个对象是相同的实例");
}
Console.Read();
}
}
class Singleton //Singleton类
{
private static Singleton instance;
private Singleton() //私有构造函数,外部无法使用new来创建该类的实例
{
}
//设置为静态方法这是获得此类实例化的唯一全局访问点
public static Singleton GetInstance()
{
if (instance == null) //这里可以保证只实例化一次,即在第一次调用的时实例化,以后调用时便不用实例化
{ //若实例不存在,在New一个新实例,否则返回已有的实例
instance = new Singleton();
}
return instance;
}
}</strong></span>
运行结果为:
从上面的结果可以看出来,尽管我两次访问了 GetInstance(),但是我访问的只是同一个实例,换句话来说,上面的代码中,由于构造函数被设置为 private 了,所以你无法再在 Singleton 类的外部使用 new 来实例化一个实例,您只能通过访问 GetInstance()来访问 Singleton 类。
GetInstance()通过如下方式保证该Singleton 只存在一个实例:
首先这个 Singleton 类会在在第一次调用 GetInstance()时创建一个实例,并将这个实例的引用封装在自身类中;
然后以后调用 GetInstance()时就会判断这个 Singleton 是否存在一个实例了,如果存在,则不会再创建实例。而是调用以前生成的类的实例,这样下来,整个应用程序中便就只存在一个实例了。
从这里再来总结单例模式的特点:
首先,单例模式使类在程序生命周期的任何时刻都只有一个实例。
然后,单例的构造函数是私有的,外部程序如果想要访问这个单例类的话,必须通过 GetInstance()来请求(注意是请求)得到这个单例类的实例。
到这里,单例模式的核心内容就介绍的差不多了。但是,要想真正的用好单例模式,就必须要深一步的去扩展和研究。
多线程时的单例
下面就来看一种情况(这里先假设我的应用程序是多线程应用程序),同时还是以前面的 Demo 来做为说明。
如果在一开始调用 GetInstance()时,是由两个线程同时调用的(这种情况是很常见的),注意是同时,(或者是一个线程进入 if 判断语句后但还没有实例化 Singleton 时,第二个线程到达,此时singleton 还是为 null)这样的话,两个线程均会进入 GetInstance(),而后由于是第一次调用 GetInstance(),所以存储在 Singleton 中的静态变量 singleton 为 null ,这样的话,就会让两个线程均通过 if 语句的条件判断,然后调用 new Singleton()了。这样的话,问题就出来了,因为有两个线程,所以会创建两个实例。很显然,这便违法了单例模式的初衷了,那么如何解决上面出现的这个问题(即多线程下使用单例模式时有可能会创建多个实例这一现象)呢?
其实, 你可以这样思考这个问题:
由于上面出现的问题中涉及到多个线程同时访问这个 GetInstance(),那么你可以先将一个线程锁定,然后等这个线程完成以后,再让其他的线程访问 GetInstance()中的 if 段语句。
比如,有两个线程同时到达,如果 singleton != null 的话,那么上面提到的问题是不会存在的,因为已经存在这个实例了,这样的话,所有的线程都无法进入 if 语句块,也就是所有的线程都无法调用语句 new Singleton()了,这样还是可以保证应用程序生命周期中的实例只存在一个。但是如果此时的singleton == null 的话,那么意味着这两个线程都是可以进入这个 if 语句块的,那么就有可能出现上面出现的单例模式中有多个实例的问题,
此时,我可以让一个线程先进入 if 语句块,然后我在外面对这个 if 语句块加锁。对第二个线程呢,由于 if 语句进行了加锁处理,所以这个进程就无法进入 if 语句块而处于阻塞状态,当进入了 if 语句块的线程完成 new Singleton()后,这个线程便会退出 if 语句块。此时,第二个线程就从阻塞状态中恢复,即就可以访问 if 语句块了,但是由于前面的那个线程已近创建了 Singleton 的实例。所以 singleton != null ,此时,第二个线程便无法通过 if 语句的判断条件了。即无法进入 if 语句块了,这样便保证了整个生命周期中只存在一个实例。也就是只有第一个线程创建了 Singleton 实例,第二个线程则无法创建实例。
下面就来重新改进前面 Demo 中的 Singleton 类,使其在多线程的环境下也可以实现单例模式的功能。
<pre name="code" class="csharp"><span style="font-family:KaiTi_GB2312;font-size:18px;"><strong> class Singleton //Singleton类
{
private static Singleton instance;
private static readonly object syncRoot = new object(); //定义一个只读静态对象,且这个对象是在程序运行时创建的
private Singleton() //私有构造函数,外部无法使用new来创建该类的实例
{
}
//设置为静态方法这是获得此类实例化的唯一全局访问点
public static Singleton GetInstance()
{
if (instance == null)
{
lock (syncRoot)
{
if (instance == null) //这里可以保证只实例化一次,即在第一次调用的时实例化,以后调用时便不用实例化
{ //若实例不存在,在New一个新实例,否则返回已有的实例
instance = new Singleton();
}
}
}
return instance;
}
}</strong></span>
上面的就是改进后的代码,可以看到在类中有定义了一个静态的只读对象 syncObject,这里需要说明的是,为何还要创建一个syncObject 静态只读对象呢?
由于提供给 lock 关键字的参数必须为基于引用类型的对象,该对象用来定义锁的范围,所以这个引用类型的对象总不能为 null 吧。而一开始的时候,singleton 为 null ,所以是无法实现加锁的。所以必须要再创建一个对象即 syncObject 来定义加锁的范围。
还有要解释一下的就是在 GetInstance()中,为什么要在 if 语句中使用两次判断 singleton == null ?这里涉及到一个名词 Double-CheckLocking ,也就是双重检查锁定。为何要使用双重检查锁定呢?
考虑这样一种情况,就是有两个线程同时到达,即同时调用 GetInstance(),此时由于 singleton == null ,所以很明显,两个线程都可以通过第一重的singleton == null ,进入第一重 if 语句后,由于存在锁机制,所以会有一个线程进入 lock 语句并进入第二重 singleton == null ,而另外的一个线程则会在 lock 语句的外面等待。而当第一个线程执行完 new Singleton()语句后,便会退出锁定区域,此时,第二个线程便可以进入 lock 语句块。此时,如果没有第二重 singleton == null 的话,那么第二个线程还是可以调用 new Singleton()语句,这样第二个线程也会创建一个 Singleton 实例,这样也还是违背了单例模式的初衷的,所以这里必须要使用双重检查锁定。
当然,如果我去掉第一重 singleton == null ,程序还是可以在多线程下完好的运行的。考虑在没有第一重 singleton == null 的情况下,当有两个线程同时到达,此时,由于lock 机制的存在,第一个线程会进入 lock 语句块,并且可以顺利执行 new Singleton()。当第一个线程退出 lock 语句块时, singleton 这个静态变量已不为 null 了,所以当第二个线程进入 lock 时,还是会被第二重 singleton == null 挡在外面,而无法执行 new Singleton()。所以在没有第一重 singleton == null 的情况下,也是可以实现单例模式的。(那么为什么需要第一重 singleton == null 呢?这里就涉及一个性能问题了,因为对于单例模式的话,new Singleton()只需要执行一次就 OK 了。而如果没有第一重 singleton == null 的话,每一次有线程进入 GetInstance()时,均会执行锁定操作来实现线程同步,这是非常耗费性能的,而如果我加上第一重 singleton == null 的话,那么就只有在第一次,也就是singleton ==null 成立时的情况下执行一次锁定以实现线程同步。而以后的话,便只要直接返回Singleton 实例就 OK 了而根本无需再进入 lock语句块了,这样就可以解决由线程同步带来的性能问题了。)
下面要介绍一下懒汉式单例和饿汉式单例
懒汉式单例
何为懒汉式单例呢,可以这样理解,单例模式呢,其在整个应用程序的生命周期中只存在一个实例。懒汉式呢,就是这个单例类的这个唯一实例是在第一次使用 GetInstance()时实例化的。如果你不调用GetInstance()的话,这个实例是不会存在的,即为 null 。形象点说呢,就是你不去动它的话,它自己是不会实例化的,所以可以称之为懒汉。
其实呢,前面在介绍单例模式的这几个 Demo 中都是使用的懒汉式单例,看下面的 GetInstance()方法就明白了:
<span style="font-family:KaiTi_GB2312;font-size:18px;"><strong> //设置为静态方法这是获得此类实例化的唯一全局访问点
public static Singleton GetInstance()
{
if (instance == null)
{
lock (syncRoot)
{
if (instance == null) //这里可以保证只实例化一次,即在第一次调用的时实例化,以后调用时便不用实例化
{ //若实例不存在,在New一个新实例,否则返回已有的实例
instance = new Singleton();
}
}
}
return instance;
}</strong></span>
从上面的这个 GetInstance()中可以看出这个单例类的唯一实例是在第一次调用GetInstance()时实例化的,所以此为懒汉式单例。
饿汉式单例
上面介绍了饿汉式单例,到这里来理解懒汉式单例的话,就容易多了。懒汉式单例由于人懒,所以其自己是不会主动实例化单例类的唯一实例的,而饿汉式的话,则刚好相反。其由于肚子饿了,所以到处找东西吃,人也变得主动了很多,所以根本就不需要别人来催他实例化单例类的为一实例,其自己就会主动实例化单例类的这个唯一类。
在 C# 中,可以用特殊的方式实现饿汉式单例,即使用静态初始化来完成饿汉式单例模式
下面就来看一看饿汉式单例类
<span style="font-family:KaiTi_GB2312;font-size:18px;"><strong> public sealed class Singleton
{
private static readonly Singleton instance = new Singleton();
private Singleton() { }
public static Singleton GetInstance()
{
return instance;
}
}</strong></span>
上面的饿汉式单例类中可以看到,当整个类被加载的时候,就会自行初始化 singleton 这个静态只读变量。而非在第一次调用 GetInstance()时再来实例化单例类的唯一实例,所以这就是一种饿汉式的单例类。
好,到这里,就真正的把单例模式介绍完了,在此呢再总结一下单例类需要注意的几点:
一、单例模式是用来实现在整个程序中只有一个实例的。
二、单例类的构造函数必须为私有,同时单例类必须提供一个全局访问点。
三、单例模式在多线程下的同步问题和性能问题的解决。
四、懒汉式和饿汉式单例类。
五、C# 中使用静态初始化实现饿汉式单例类。