【总结】设计模式应用之单例模式

一、前言

单例模式的应用场景十分清晰,就是一句话,在整个的软件运行周期内,对于某个类只能允许有零个或一个实例。单例模式应用十分广泛,比如我们电脑上的任务管理器就是一个单例模式,无论开多少个任务管理器,你会发现只有一个窗口,这就是典型的单例模式的应用;还有,网站的访问次数统计,如果不采用单例模式会很难统计;多线程的线程池的设计一般也是采用单例模式,这是由于线程池要方便对池中的线程进行控制;Web应用的配置对象的读取,一般也应用单例模式,这是由于配置文件是共享的资源;HttpApplication 也是单位例的典型应用,如果你了解ASP.Net(IIS)的整个请求生命周期,就会发现其实HttpApplication也是单例模式,所有的HttpModule都共享一个HttpApplication实例。这样的例子实在太多了,那么比较一下就会发现这些例子的共同点:资源共享的时候,适合使用单例,避免多个对象竞争产生死锁,虽然可以不使用单例,但是对性能还是或多或少有些影响的。

二、单例模式的实现

1、单例模式的构造函数是私有的,为了避免外部使用new操作符进行实例化而违背这一设计的初衷;

2、单例模式必须有一个全局访问点,用于获取当前的实例;

既然单例模式是这样,那就写一下代码看看:

 1 public class SingleTon
 2 {
 3     private static SingleTon _singleTon;
 4     //私有构造,避免new操作符
 5     private SingleTon()
 6     {
 7 
 8     }
 9 
10     public static SingleTon GetSingleInstance()
11     {
12         //如果_singleTon是空的,那么创建一个实例并返回
13         if (_singleTon == null)
14         {
15             _singleTon = new SingleTon();
16         }
17         return _singleTon;
18     }
19 }

下面是客户端调用代码:

 1 class Program
 2 {
 3     static void Main(string[] args)
 4     {
 5         SingleTon s1 = SingleTon.GetSingleInstance();
 6         SingleTon s3 = SingleTon.GetSingleInstance();
 7         Console.WriteLine(s1.Equals(s3));//返回True
 8         Console.ReadLine();
 9     }
10 }

运行结果返回True,这是很明显的。因为SingleTon第一次调用该类的GetSingleInstance方法时,进入到了if语句块并创建了实例,而再次调用GetSingleInstance方法的时候,已经存在实例了,就直接把_singleTon返回了,所以s1和s2指向的其实是同一个实例。你以为单例模式就这么完事儿了?远没有这么简单,上面的情形只是在单线程情况下,假设这样一个场景,如果在多线程的应用程序中,恰好有两个线程同时访问该类的GetSingleInstance方法,当第一个线程进入的时候,此时的_singleTon还是null,第二个线程进入的时机就是在第一个线程已经进入if语句块但是还没有创建实例的时候,此时问题就来了,两个线程进入了if语句那么必然会创建两个实例,很明显这不符合单例模式的逻辑。那怎么样才能避免上面的问题呢?加锁。

三、多线程下的单例模式

多线程中,使用单例模式需要处理的就是如何避免多个线程创建多个实例的问题。可以采用锁机制来进行控制,代码如下:

 1 public class SingleTon
 2 {
 3     private static SingleTon _singleTon;
 4     private static readonly SingleTon _single = new SingleTon();
 5     private static readonly object _lock = new object();
 6     //私有构造,避免new操作符
 7     private SingleTon()
 8     {
 9 
10     }
11 
12     public static SingleTon GetSingleInstance()
13     {
14         if (_singleTon == null)
15         {
16             lock (_lock)
17             {
18                 if (_singleTon == null)
19                 {
20                     _singleTon = new SingleTon();
21                 }
22             }
23         }
24         return _singleTon;
25     }
26 
27     public static SingleTon GetSingleTonInstance()
28     {
29         return _single;
30     }
31 }

细心的你可能会发现这段代码和上面的代码相比,除了加了锁,还有一个变化,就是多了一层非空判断。下面我阐述下为什么这样做,加锁很容易理解,就是避免多个线程同时进入if语句创建多个实例,试想一下如果不加第一层if非空判断会发生什么?假设有两个线程A和B,A和B线程同时访问GetSingleInstance方法,A先进入,由于加了锁,所以B必须等待A完成操作退出后才可以进入,现在A已经成功访问创建实例的方法,因此_singleTon此时不为空,B线程可以进入,发现_singleTon不为空了,直接退出。以上是A和B第一次访问,接着又来了两个线程C和D,同样是C先进入lock语句块,发现_singleTon此时不为空并退出,此时等待中的D可以进入了,和C发生了同样的遭遇。不知道你看出问题没有,既然第一次已经成功创建实例了,后续就没有必要再进入lock块了,因为不论你进或者不进入,被创建的实例就在那里。其实最外层的非空判断加与不加都不会影响单例的创建,所以最外层的非空判断核心作用就是提升性能,避免没有实际意义的操作。在我的另一篇博文[设计模式应用之策略模式]中涉及到主题管理,负责主题管理的类就是一个单例模式。

四、懒汉式和饿汉式单例

懒汉模式就是第三部分的示例代码,在这里先把饿汉式单例的实现代码贴出来进行比较:

 1 public class SingleTon
 2 {
 3     private static readonly SingleTon _single = new SingleTon();
 4     //私有构造,避免new操作符
 5     private SingleTon()
 6     {
 7 
 8     }
 9 
10     public static SingleTon GetSingleTonInstance()
11     {
12         return _single;
13     }
14 }

饿汉式单例相对于懒汉式单例就简单多了,它直接在类的内部new一个SingleTon放在那,如果需要的时候就返回这个实例;反过来看一下懒汉式单例,它是在需要的时候,再去new一个SingleTon然后再返回这个实例。读到这也许你就会明白这两个名称的由来了,饿汉式单例是比较积极的,我知道你要来拿这个实例,我提前给你准备好,你来的时候,我就直接给你;而懒汉式则比较懒,不到最后我是不会创建实例的,你来的时候我现场创建一个再给你。

五、总结

单例模式是最为常用的设计模式之一,应用场景非常地广,所以使用的时候,需要注意一下几点:

  • 单例模式和核心就是程序的整个运行周期内只有不多于一个实例;
  • 私有化构造函数并创建一个公开的静态的全局访问点;
  • 如果是在多线程下使用,应该注意加锁,以及双重非空校验;
  • 理解懒汉式单例和饿汉式单例;

对于设计模式,不需要死记硬背,只要记住几个关键的点,然后在实际的编码中多进行应用,就可以掌握。

 

posted @ 2017-01-10 08:27  悠扬的牧笛  阅读(545)  评论(0编辑  收藏  举报