返回
前端
分类

这种重复性模式概念在其他应用中是非常明显的,提供一种机制来保证一个类只有一个实例

日期: 2020-03-13 22:15 浏览次数 : 144

[简介]
上期我们刊登了Jeffrey Richter的《探究Observer模式》,详细讲解了在微软架构下实现Observer模式的技术和经验。本期我们继续探讨设计模式方面的话题。Singleton通常被认为是最简单的设计模式,很多初学者都是通过它来了解设计模式的含义。然而,熟悉设计模式的技术人员都知道,要正确实现Singleton模式实际上是非常难的,涉及到很多技术细节。本文对于Singleton做了大胆深入的研究,并且探讨了C++、Java和C#中的Singleton实现。

单件模式(Singleton Pattern)

——.NET设计模式系列之二

Terrylee,2005年12月07日

概述

Singleton模式要求一个类有且仅有一个实例,并且提供了一个全局的访问点。这就提出了一个问题:如何绕过常规的构造器,提供一种机制来保证一个类只有一个实例?客户程序在调用某一个类时,它是不会考虑这个类是否只能有一个实例等问题的,所以,这应该是类设计者的责任,而不是类使用者的责任。

从另一个角度来说,Singleton模式其实也是一种职责型模式。因为我们创建了一个对象,这个对象扮演了独一无二的角色,在这个单独的对象实例中,它集中了它所属类的所有权力,同时它也肩负了行使这种权力的职责!

意图

保证一个类仅有一个实例,并提供一个访问它的全局访问点。

模型图

逻辑模型图:

必赢备用网址 1

物理模型图:

必赢备用网址 2

生活中的例子

美国总统的职位是Singleton,美国宪法规定了总统的选举,任期以及继任的顺序。这样,在任何时刻只能由一个现任的总统。无论现任总统的身份为何,其头衔"美利坚合众国总统"是访问这个职位的人的一个全局的访问点。

必赢备用网址 3

五种实现

1.简单实现

 

 

 1必赢备用网址 4public sealed class Singleton
 2必赢备用网址 5必赢备用网址 6必赢备用网址 7{
 3必赢备用网址 8    static Singleton instance=null;
 4必赢备用网址 9
 5必赢备用网址 10    Singleton()
 6必赢备用网址 11必赢备用网址 12    必赢备用网址 13{
 7必赢备用网址 14必赢备用网址 ,    }
 8必赢备用网址 15
 9必赢备用网址 16    public static Singleton Instance
10必赢备用网址 17必赢备用网址 18    必赢备用网址 19{
11必赢备用网址 20        get
12必赢备用网址 21必赢备用网址 22        必赢备用网址 23{
13必赢备用网址 24            if (instance==null)
14必赢备用网址 25必赢备用网址 26            必赢备用网址 27{
15必赢备用网址 28                instance = new Singleton();
16必赢备用网址 29            }
17必赢备用网址 30            return instance;
18必赢备用网址 31        }
19必赢备用网址 32    }
20必赢备用网址 33}

这种方式的实现对于线程来说并不是安全的,因为在多线程的环境下有可能得到Singleton类的多个实例。如果同时有两个线程去判断(instance == null),并且得到的结果为真,这时两个线程都会创建类Singleton的实例,这样就违背了Singleton模式的原则。实际上在上述代码中,有可能在计算出表达式的值之前,对象实例已经被创建,但是内存模型并不能保证对象实例在第二个线程创建之前被发现。

 

该实现方式主要有两个优点:

l         由于实例是在 Instance 属性方法内部创建的,因此类可以使用附加功能(例如,对子类进行实例化),即使它可能引入不想要的依赖性。

l         直到对象要求产生一个实例才执行实例化;这种方法称为“惰性实例化”。惰性实例化避免了在应用程序启动时实例化不必要的 singleton

2.安全的线程

 1必赢备用网址 34public sealed class Singleton
 2必赢备用网址 35必赢备用网址 36必赢备用网址 37{
 3必赢备用网址 38    static Singleton instance=null;
 4必赢备用网址 39    static readonly object padlock = new object();
 5必赢备用网址 40
 6必赢备用网址 41    Singleton()
 7必赢备用网址 42必赢备用网址 43    必赢备用网址 44{
 8必赢备用网址 45    }
 9必赢备用网址 46
10必赢备用网址 47    public static Singleton Instance
11必赢备用网址 48必赢备用网址 49    必赢备用网址 50{
12必赢备用网址 51        get
13必赢备用网址 52必赢备用网址 53        必赢备用网址 54{
14必赢备用网址 55            lock (padlock)
15必赢备用网址 56必赢备用网址 57            必赢备用网址 58{
16必赢备用网址 59                if (instance==null)
17必赢备用网址 60必赢备用网址 61                必赢备用网址 62{
18必赢备用网址 63                    instance = new Singleton();
19必赢备用网址 64                }
20必赢备用网址 65                return instance;
21必赢备用网址 66            }
22必赢备用网址 67        }
23必赢备用网址 68    }
24必赢备用网址 69}
25必赢备用网址 70
26必赢备用网址 71

 

这种方式的实现对于线程来说是安全的。我们首先创建了一个进程辅助对象,线程在进入时先对辅助对象加锁然后再检测对象是否被创建,这样可以确保只有一个实例被创建,因为在同一个时刻加了锁的那部分程序只有一个线程可以进入。这种情况下,对象实例由最先进入的那个线程创建,后来的线程在进入时(instence == null)为假,不会再去创建对象实例了。但是这种实现方式增加了额外的开销,损失了性能。

3.双重锁定

 1必赢备用网址 72public sealed class Singleton
 2必赢备用网址 73必赢备用网址 74必赢备用网址 75{
 3必赢备用网址 76    static Singleton instance=null;
 4必赢备用网址 77    static readonly object padlock = new object();
 5必赢备用网址 78
 6必赢备用网址 79    Singleton()
 7必赢备用网址 80必赢备用网址 81    必赢备用网址 82{
 8必赢备用网址 83    }
 9必赢备用网址 84
10必赢备用网址 85    public static Singleton Instance
11必赢备用网址 86必赢备用网址 87    必赢备用网址 88{
12必赢备用网址 89        get
13必赢备用网址 90必赢备用网址 91        必赢备用网址 92{
14必赢备用网址 93            if (instance==null)
15必赢备用网址 94必赢备用网址 95            必赢备用网址 96{
16必赢备用网址 97                lock (padlock)
17必赢备用网址 98必赢备用网址 99                必赢备用网址 100{
18必赢备用网址 101                    if (instance==null)
19必赢备用网址 102必赢备用网址 103                    必赢备用网址 104{
20必赢备用网址 105                        instance = new Singleton();
21必赢备用网址 106                    }
22必赢备用网址 107                }
23必赢备用网址 108            }
24必赢备用网址 109            return instance;
25必赢备用网址 110        }
26必赢备用网址 111    }
27必赢备用网址 112}
28必赢备用网址 113

这种实现方式对多线程来说是安全的,同时线程不是每次都加锁,只有判断对象实例没有被创建时它才加锁,有了我们上面第一部分的里面的分析,我们知道,加锁后还得再进行对象是否已被创建的判断。它解决了线程并发问题,同时避免在每个 Instance这种重复性模式概念在其他应用中是非常明显的,提供一种机制来保证一个类只有一个实例。 属性方法的调用中都出现独占锁定。它还允许您将实例化延迟到第一次访问对象时发生。实际上,应用程序很少需要这种类型的实现。大多数情况下我们会用静态初始化。这种方式仍然有很多缺点:无法实现延迟初始化。

4.静态初始化

 1必赢备用网址 114public sealed class Singleton
 2必赢备用网址 115必赢备用网址 116必赢备用网址 117{
 3必赢备用网址 118    static readonly Singleton instance=new Singleton();
 4必赢备用网址 119
 5必赢备用网址 120    static Singleton()
 6必赢备用网址 121必赢备用网址 122    必赢备用网址 123{
 7必赢备用网址 124    }
 8必赢备用网址 125
 9必赢备用网址 126    Singleton()
10必赢备用网址 127必赢备用网址 128    必赢备用网址 129{
11必赢备用网址 130    }
12必赢备用网址 131
13必赢备用网址 132    public static Singleton Instance
14必赢备用网址 133必赢备用网址 134    必赢备用网址 135{
15必赢备用网址 136        get
16必赢备用网址 137必赢备用网址 138        必赢备用网址 139{
17必赢备用网址 140            return instance;
18必赢备用网址 141        }
19必赢备用网址 142    }
20必赢备用网址 143}
21必赢备用网址 144

看到上面这段富有戏剧性的代码,我们可能会产生怀疑,这还是Singleton模式吗?在此实现中,将在第一次引用类的任何成员时创建实例。公共语言运行库负责处理变量初始化。该类标记为 sealed以阻止发生派生,而派生可能会增加实例。此外,变量标记为 readonly,这意味着只能在静态初始化期间(此处显示的示例)或在类构造函数中分配变量。

该实现与前面的示例类似,不同之处在于它依赖公共语言运行库来初始化变量。它仍然可以用来解决 Singleton模式试图解决的两个基本问题:全局访问和实例化控制。公共静态属性为访问实例提供了一个全局访问点。此外,由于构造函数是私有的,因此不能在类本身以外实例化 Singleton类;因此,变量引用的是可以在系统中存在的唯一的实例。

由于 Singleton实例被私有静态成员变量引用,因此在类首次被对 Instance 属性的调用所引用之前,不会发生实例化。

这种方法唯一的潜在缺点是,您对实例化机制的控制权较少。在 Design Patterns* *形式中,您能够在实例化之前使用非默认的构造函数或执行其他任务。由于在此解决方案中由 .NET Framework负责执行初始化,因此您没有这些选项。在大多数情况下,静态初始化是在 .NET 中实现 Singleton 的首选方法。

5.延迟初始化

 1必赢备用网址 145public sealed class Singleton
 2必赢备用网址 146必赢备用网址 147必赢备用网址 148{
 3必赢备用网址 149    Singleton()
 4必赢备用网址 150必赢备用网址 151    必赢备用网址 152{
 5必赢备用网址 153    }
 6必赢备用网址 154
 7必赢备用网址 155    public static Singleton Instance
 8必赢备用网址 156必赢备用网址 157    必赢备用网址 158{
 9必赢备用网址 159        get
10必赢备用网址 160必赢备用网址 161        必赢备用网址 162{
11必赢备用网址 163            return Nested.instance;
12必赢备用网址 164        }
13必赢备用网址 165    }
14必赢备用网址 166    
15必赢备用网址 167    class Nested
16必赢备用网址 168必赢备用网址 169    必赢备用网址 170{
17必赢备用网址 171        static Nested()
18必赢备用网址 172必赢备用网址 173        必赢备用网址 174{
19必赢备用网址 175        }
20必赢备用网址 176
21必赢备用网址 177        internal static readonly Singleton instance = new Singleton();
22必赢备用网址 178    }
23必赢备用网址 179}
24必赢备用网址 180

这里,初始化工作有Nested类的一个静态成员来完成,这样就实现了延迟初始化,并具有很多的优势,是值得推荐的一种实

现方式。

实现要点

l        Singleton模式是限制而不是改进类的创建。

l         Singleton类中的实例构造器可以设置为Protected以允许子类派生。

l         Singleton模式一般不要支持Icloneable接口,因为这可能导致多个对象实例,与Singleton模式的初衷违背。

l         Singleton模式一般不要支持序列化,这也有可能导致多个对象实例,这也与Singleton模式的初衷违背。

l         Singleton只考虑了对象创建的管理,没有考虑到销毁的管理,就支持垃圾回收的平台和对象的开销来讲,我们一般没必要对其销毁进行特殊的管理。

l         理解和扩展Singleton模式的核心是“如何控制用户使用new对一个类的构造器的任意调用”。

 

 

l         可以很简单的修改一个Singleton,使它有少数几个实例,这样做是允许的而且是有意义的。

优点

l         实例控制:Singleton会阻止其他对象实例化其自己的 Singleton 对象的副本,从而确保所有对象都访问唯一实例

l         灵活性:因为类控制了实例化过程,所以类可以更加灵活修改实例化过程

缺点

l         开销:虽然数量很少,但如果每次对象请求引用时都要检查是否存在类的实例,将仍然需要一些开销。可以通过使用静态初始化解决此问题,上面的五种实现方式中已经说过了。

l          可能的开发混淆:使用 singleton 对象(尤其在类库中定义的对象)时,开发人员必须记住自己不能使用 new 关键字实例化对象。因为可能无法访问库源代码,因此应用程序开发人员可能会意外发现自己无法直接实例化此类。

l         对象的生存期:Singleton不能解决删除单个对象的问题。在提供内存管理的语言中(例如基于 .NET Framework 的语言),只有 Singleton 类能够导致实例被取消分配,因为它包含对该实例的私有引用。在某些语言中(如 C++),其他类可以删除
对象实例,但这样会导致 Singleton 类中出现悬浮引用。

适用性

l         当类只能有一个实例而且客户可以从一个众所周知的访问点访问它时。

l         当这个唯一实例应该是通过子类化可扩展的,并且客户应该无需更改代码就能使用一个扩展的实例时。

应用场景

l         每台计算机可以有若干个打印机,但只能有一个Printer Spooler,避免两个打印作业同时输出到打印机。
(摘自吕震宇的C#设计模式(7)-Singleton Pattern)

l         PC机中可能有几个串口,但只能有一个COM1口的实例。

l         系统中只能有一个窗口管理器。

l         .NET Remoting中服务器激活对象中的Sigleton对象,确保所有的客户程序的请求都只有一个实例来处理。

完整示例

这是一个简单的计数器例子,四个线程同时进行计数。

 1必赢备用网址 181using System;
 2必赢备用网址 182using System.Threading;
 3必赢备用网址 183
 4必赢备用网址 184namespace SigletonPattern.SigletonCounter
 5必赢备用网址 185必赢备用网址 186必赢备用网址 187{
 6必赢备用网址 188必赢备用网址 189    /**//// <summary>
 7必赢备用网址 190    /// 功能:简单计数器的单件模式
 8必赢备用网址 191    /// 编写:Terrylee
 9必赢备用网址 192    /// 日期:2005年12月06日
10必赢备用网址 193    /// </summary>
11必赢备用网址 194    public class CountSigleton
12必赢备用网址 195必赢备用网址 196    必赢备用网址 197{
13必赢备用网址 198必赢备用网址 199        /**////存储唯一的实例
14必赢备用网址 200        static CountSigleton uniCounter = new CountSigleton();  
15必赢备用网址 201   
16必赢备用网址 202必赢备用网址 203        /**////存储计数值
17必赢备用网址 204        private int totNum = 0;  
18必赢备用网址 205   
19必赢备用网址 206        private CountSigleton() 
20必赢备用网址 207   
21必赢备用网址 208必赢备用网址 209        必赢备用网址 210
22必赢备用网址 211必赢备用网址 212            /**////线程延迟2000毫秒
23必赢备用网址 213            Thread.Sleep(2000);
24必赢备用网址 214        } 
25必赢备用网址 215   
26必赢备用网址 216        static public CountSigleton Instance() 
27必赢备用网址 217   
28必赢备用网址 218必赢备用网址 219        必赢备用网址 220
29必赢备用网址 221   
30必赢备用网址 222            return uniCounter; 
31必赢备用网址 223   
32必赢备用网址 224        } 
33必赢备用网址 225        
34必赢备用网址 226必赢备用网址 227        /**////计数加1
35必赢备用网址 228        public void Add()
36必赢备用网址 229必赢备用网址 230        必赢备用网址 231
37必赢备用网址 232            totNum ++;
38必赢备用网址 233        }  
39必赢备用网址 234        
40必赢备用网址 235必赢备用网址 236        /**////获得当前计数值
41必赢备用网址 237        public int GetCounter()
42必赢备用网址 238必赢备用网址 239        必赢备用网址 240
43必赢备用网址 241            return totNum;
44必赢备用网址 242        } 
45必赢备用网址 243
46必赢备用网址 244    }
47必赢备用网址 245}
48必赢备用网址 246

 

 1必赢备用网址 247using System;
 2必赢备用网址 248using System.Threading;
 3必赢备用网址 249using System.Text;
 4必赢备用网址 250
 5必赢备用网址 251namespace SigletonPattern.SigletonCounter
 6必赢备用网址 252必赢备用网址 253必赢备用网址 254{
 7必赢备用网址 255必赢备用网址 256    /**//// <summary>
 8必赢备用网址 257    /// 功能:创建一个多线程计数的类
 9必赢备用网址 258    /// 编写:Terrylee
10必赢备用网址 259    /// 日期:2005年12月06日
11必赢备用网址 260    /// </summary>
12必赢备用网址 261    public class CountMutilThread
13必赢备用网址 262必赢备用网址 263    必赢备用网址 264{
14必赢备用网址 265        public CountMutilThread()
15必赢备用网址 266必赢备用网址 267        必赢备用网址 268{
16必赢备用网址 269            
17必赢备用网址 270        }
18必赢备用网址 271
19必赢备用网址 272必赢备用网址 273        /**//// <summary>
20必赢备用网址 274        /// 线程工作
21必赢备用网址 275        /// </summary>
22必赢备用网址 276        public static void DoSomeWork()
23必赢备用网址 277必赢备用网址 278        必赢备用网址 279{
24必赢备用网址 280必赢备用网址 281            /**////构造显示字符串
25必赢备用网址 282            string results = "";
26必赢备用网址 283
27必赢备用网址 284必赢备用网址 285            /**////创建一个Sigleton实例
28必赢备用网址 286            CountSigleton MyCounter = CountSigleton.Instance();
29必赢备用网址 287
30必赢备用网址 288必赢备用网址 289            /**////循环调用四次
31必赢备用网址 290            for(int i=1;i<5;i++)
32必赢备用网址 291必赢备用网址 292            必赢备用网址 293{
33必赢备用网址 294必赢备用网址 295                /**////开始计数
34必赢备用网址 296                MyCounter.Add();
35必赢备用网址 297                
36必赢备用网址 298                results +="线程";
37必赢备用网址 299                results += Thread.CurrentThread.Name.ToString() + "——〉";
38必赢备用网址 300                results += "当前的计数:";
39必赢备用网址 301                results += MyCounter.GetCounter().ToString();
40必赢备用网址 302                results += "n";
41必赢备用网址 303
42必赢备用网址 304                Console.WriteLine(results);
43必赢备用网址 305                
44必赢备用网址 306必赢备用网址 307                /**////清空显示字符串
45必赢备用网址 308                results = "";
46必赢备用网址 309            }
47必赢备用网址 310        }
48必赢备用网址 311
49必赢备用网址 312        public void StartMain()
50必赢备用网址 313必赢备用网址 314        必赢备用网址 315{
51必赢备用网址 316
52必赢备用网址 317            Thread thread0 = Thread.CurrentThread; 
53必赢备用网址 318   
54必赢备用网址 319            thread0.Name = "Thread 0"; 
55必赢备用网址 320   
56必赢备用网址 321            Thread thread1 =new Thread(new ThreadStart(DoSomeWork)); 
57必赢备用网址 322   
58必赢备用网址 323            thread1.Name = "Thread 1"; 
59必赢备用网址 324   
60必赢备用网址 325            Thread thread2 =new Thread(new ThreadStart(DoSomeWork)); 
61必赢备用网址 326   
62必赢备用网址 327            thread2.Name = "Thread 2"; 
63必赢备用网址 328   
64必赢备用网址 329            Thread thread3 =new Thread(new ThreadStart(DoSomeWork)); 
65必赢备用网址 330   
66必赢备用网址 331            thread3.Name = "Thread 3"; 
67必赢备用网址 332   
68必赢备用网址 333            thread1.Start(); 
69必赢备用网址 334   
70必赢备用网址 335            thread2.Start(); 
71必赢备用网址 336   
72必赢备用网址 337            thread3.Start(); 
73必赢备用网址 338            
74必赢备用网址 339必赢备用网址 340            /**////线程0也只执行和其他线程相同的工作
75必赢备用网址 341            DoSomeWork(); 
76必赢备用网址 342        }
77必赢备用网址 343    }
78必赢备用网址 344}
79必赢备用网址 345

 

 1必赢备用网址 346using System;
 2必赢备用网址 347using System.Text;
 3必赢备用网址 348using System.Threading;
 4必赢备用网址 349
 5必赢备用网址 350namespace SigletonPattern.SigletonCounter
 6必赢备用网址 351必赢备用网址 352必赢备用网址 353{
 7必赢备用网址 354必赢备用网址 355    /**//// <summary>
 8必赢备用网址 356    /// 功能:实现多线程计数器的客户端
 9必赢备用网址 357    /// 编写:Terrylee
10必赢备用网址 358    /// 日期:2005年12月06日
11必赢备用网址 359    /// </summary>
12必赢备用网址 360    public class CountClient
13必赢备用网址 361必赢备用网址 362    必赢备用网址 363{
14必赢备用网址 364        public static void Main(string[] args)
15必赢备用网址 365必赢备用网址 366        必赢备用网址 367{
16必赢备用网址 368       CountMutilThread cmt = new CountMutilThread();
17必赢备用网址 369
18必赢备用网址 370            cmt.StartMain();
19必赢备用网址 371
20必赢备用网址 372            Console.ReadLine();
21必赢备用网址 373        }
22必赢备用网址 374    }
23必赢备用网址 375}
24必赢备用网址 376

 

 

**总结**

Singleton设计模式是一个非常有用的机制,可用于在面向对象的应用程序中提供单个访问点。文中通过五种实现方式的比较和一个完整的示例,完成了对Singleton模式的一个总结和探索。用一句广告词来概括Singleton模式就是“简约而不简单”。

_________________________________________________________________________________________________
源码下载:/Files/Terrylee/SigletonPattern.rar

参考文献:

《C#计模式》,中国电力出版社

使用 Microsoft .NET 的企业解决方案模式

《Implementing the Singleton Pattern in C#》

MSDN《Exploring the Singleton Design Pattern》

吕震宇C#设计模式(7)-Singleton Pattern

C#的Singleton设计模式

在开发软件应用程序过程中,随着应用程序的开发,会出现重复性的模式。随着整个软件系统的开发,很多相同的模式会逐渐显现出来。
这种重复性模式概念在其他应用中是非常明显的。汽车制造就是一种此类应用。很多不同的汽车型号使用相同的子构件,包括大多数基本部件(例如,灯泡和紧固零件)以及较大的构件(例如,底盘和发动机)。
在住宅建筑中,重复性模式概念适用于螺丝和螺钉以及整体总体建筑物配电系统。无论组建的小组是为了开发新的汽车设计还是新的建筑物设计,它通常不必没有考虑到以前已解决的问题。如果设计和建筑住宅的小组必须重新构思和设计房子的每一个组成部分,则整个过程所花的时间比现在要长得多。门高或灯开关功能等许多设计决策(例如,门高或灯开关功能)很容易理解。房为满足给房子不同部分提供洗手功能的要求,房屋设计师不必重新设计和重新建造不同类型的输供水和蓄水设施,以便达到为房子不同部分提供洗手功能的要求:标准水槽以及标准的热水和冷水输入接头和排水输出接头是很容易理解非常常见的房屋建筑构件。可以将重复性模式概念反复应用于我们周围的几乎每样东西上,包括软件。
汽车和住宅建筑示例有助于在软件设计和构造中体现某些一般性的抽象概念。易于理解且明确定义的通用功能部件的概念是设计模式的源动力,它也是其他两篇设计模式文章探究工厂设计模式和探究观察者设计模式的重点。这些模式几乎涵盖了面向对象的软件设计的各个方面,包括对象创建、对象交互和对象生存期。在本文中,我们将讨论Singleton模式,它包含在创造性模式系列中。
创造性模式指示如何以及何时创建对象。很多实例需要只能通过创造性方法解决的特殊行为,而不是在创建实例后强制实施所需的行为。此类行为要求最好的例子之一包含在Singleton模式中。Singleton模式在《设计模式:可复用的面向对象软件的基础》这一经典参考书目中有正式的定义,该书的作者包括Erich Gamma、Richard Helm、Ralph Johnson和John Vlissides(也称为四人组或GoF)。在设计模式中,此模式是最简单也是使用最广泛的模式之一。但是,正如我们将会看到的一样,在实现此模式时可能会出现一些问题。本文试图通过Singleton模式的多个早期实现来从头开始分析Singleton模式,以及如何在Microsoft .NET应用程序开发中发挥其最佳用途。

Singleton模式
按照设计模式中的定义,Singleton模式的用途是“ensure a class has only one instance, and provide a global point of access to it(确保每个类只有一个实例,并提供它的全局访问点)”。
它可以解决什么问题,或者换句话说,我们使用它的动机是什么?几乎在每个应用程序中,都需要有一个从中进行全局访问和维护某种类型数据的区域。在面向对象的(OO)系统中也有这种情况,在此类系统中,在任何给定时间只应运行一个类或某个类的一组预定义数量的实例。例如,当使用某个类来维护增量计数器时,此简单的计数器类需要跟踪在多个应用程序领域中使用的整数值。此类需要能够增加该计数器并返回当前的值。对于这种情况,所需的类行为应该仅使用一个类实例来维护该整数,而不是使用其它类实例来维护该整数。
最初,人们可能会试图将计数器类实例只作为静态全局变量来创建。这是一种通用的方法,但实际上只解决一部分问题;它解决了全局可访问性问题,但没有采取任何措施来确保在任何给定的时间只运行一个类实例。应该由类本身来负责只使用一个类实例,而不是由类用户来负责。应该始终不要让类用户来监视和控制运行的类实例的数量。
所需要的是使用某种方法来控制如何创建类实例,然后确保在任何给定的时间只创建一个类实例。这会确切地给我们提供所需的行为,并使客户端不必了解任何类细节。

逻辑模型
Singleton模型非常简单直观。(通常)只有一个Singleton实例。客户端通过一个已知的访问点来访问Singleton实例。在这种情况下,客户端是一个需要访问唯一Singleton实例的对象。图1以图形方式显示此关系。
物理模型
Singleton模式的物理模型也是非常简单的。但是,随着时间的推移,实现Singleton的方式也略有不同。让我们看一下原始的GoFSingleton实现。图2显示按设计模式所定义的原始Singleton模式的UML模型。
我们看到的是一个简单的类图表,显示有一个Singleton对象的私有静态属性以及返回此相同属性的公共方法Instance()。这实际上是Singleton的核心。还有其他一些属性和方法,用于说明在该类上允许执行的其他操作。为了便于此次讨论,让我们将重点放在实例属性和方法上。
客户端仅通过实例方法来访问任何Singleton实例。此处没有定义创建实例的方式。我们还希望能够控制如何以及何时创建实例。在OO开发中,通常可以在类的构造函数中最好地处理特殊对象的创建行为。这种情况也不例外。我们可以做的是,定义我们何时以及如何构造类实例,然后禁止任何客户端直接调用该构造函数。这是在Singleton构造中始终使用的方法。让我们看一下设计模式中的原始示例。通常,将下面所示的C++Singleton示例实现代码示例视为Singleton的默认实现。本示例已移植到很多其他编程语言中,通常它在任何地方的形式与此几乎相同。
C++Singleton示例实现代码

//Declaration
class Singleton{
public:
static Singleton* Instance();
protected:
Singleton();
private:
static Singleton* _instance;
}

// Implementation
Singleton* Singleton::_instance = 0;

Singleton* Singleton::Instance() {
if (_instance == 0) {
_instance = new Singleton;
}
return _instance;
}

让我们先花点时间分析一下此代码。该简单类有一个成员变量,此变量是指向该类自身的指针。注意,构造函数是受保护的,并且只有公共方法才是实例方法。在实例方法实现中,有一个控制块(if),它检查成员变量是否已初始化,如果没有的话,则创建一个新实例。控制块中这种惰性初始化意味着仅在第一次调用Instance()方法时初始化或创建Singleton实例。对于很多应用程序,这种方法效果很好。但对于多线程应用程序,这种方法证明具有潜在危险的副作用。如果两个线程同时进入控制块,则可能会创建该成员变量的两个实例。要解决这一问题,您可能想只将重要部分放在控制块周围以确保线程安全。如果您这样做,则将对实例方法的所有调用进行序列化处理,并且可能会对性能产生不利影响(取决于应用程序)。正是由于这个原因,创建了此模式的另一个版本,它使用某种称为双重检验机制的功能。下一个代码示例显示使用Java语法的双重检验锁定。
使用Java语法的双重检验锁定Singleton代码

//C++ port to Java
class Singleton
{
public staticSingletonInstance() {
if (_instance == null) {
synchronized (Class.forName("Singleton")) {
if (_instance == null) {
_instance = new Singleton();
}
}
}
return _instance;
}
protected Singleton() {}
private staticSingleton_instance = null;
}

在使用Java语法的双重检验锁定Singleton代码示例中,我们直接将C++代码移植到Java代码,以便利用Java关键部分块(已同步)。主要差别是不再有单独的声明和实现部分,没有指针数据类型,并且采用了新的双重检验机制。双重检验发生在第一个IF块上。如果成员变量为空,则执行进入关键部分块,该块再次双重检验该成员变量。仅在通过此最终测试后,才会实例化该成员变量。一般来说,两个线程无法使用这种方法创建两个类实例。另外,因为在第一次检查时没有出现线程阻塞,所以对此方法的大多数调用不会由于必须进入锁定而导致性能下降。目前,在实现Singleton模式时,很多Java应用程序中都广泛使用这种方法。这种方法很巧妙,但也有瑕疵。某些优化编译器可以将惰性初始化代码优化掉或对其重新进行排序,并且会重新产生线程安全问题。有关更深入的解释,请参阅"The Double-Check Locking is Broken" (
另一种试图解决此问题的方法可能是,在成员变量声明中使用volatile关键字。这应该告诉编译器不要对代码重新排序,并且放弃优化。目前,这是唯一建议的JVM内存模型,并且不会立即解决该问题。
实现Singleton的最好方法是什么?最终(而不是碰巧),Microsoft .NET框架解决了所有这些问题,从而更易于实现Singleton,却不会产生我们目前讨论的不利副作用。.NET框架以及C#语言允许我们在必要时通过替换语言关键字,将上述的Java语法移植到C#语法。因此,Singleton代码变为以下内容:
以C#编码的双重检验锁定

// Port to C#
class Singleton
{
public staticSingletonInstance() {
if (_instance == null) {
lock (typeof(Singleton)) {
if (_instance == null) {
_instance = new Singleton();
}
}
}
return _instance;
}
protected Singleton() {}
private static volatileSingleton_instance = null;
}

此处,我们替换了锁定关键字来执行关键部分块,使用typeof操作并添加volatile关键字,以确保没有对代码进行优化程序重新排序。虽然此代码或多或少是GoFSingleton模式的直接移植,但它可达到我们的目的,并且我们可获得所需的行为。此代码还说明了将C++移植到Java和将Java移植到C#代码的一些相似之处和主要差别。但是,正如任何代码移植一样,通常目标语言或平台的一些优点可能在移植过程中失去。需要做的就是对代码重构,以便利用新目标语言或平台的功能。
在前面的每个代码示例中,Singleton的原始实现随时间的推移而发生变化,以解决在每个新模式实现中发现的问题。一些问题(例如,线程安全)要求对大多数实现进行更改,以满足在目前应用程序中日益增长的需要并解决演变发展问题。.NET在应用程序开发中提供了一个演变步骤。可以在“框架”级别解决前面示例中出现的很多亟待解决的问题,而不是在实现级别解决。虽然上一个示例显示了一个使用.NET框架和C#的有效Singleton类,但只需更好地利用.NET框架本身就可以大大简化此代码。以下示例使用.NET,它是一个松散地基于原始GoF模式的最小限度的Singleton类,并且仍然可获得类似的行为。
.NETSingleton示例
//.NET Singleton
sealed class Singleton
{
private Singleton() {}
public static readonlySingletonInstance = new Singleton();
}

此版本已大大简化并且更加直观。它仍然是Singleton吗?让我们看一下更改了哪些内容,然后再做决定。我们修改了要密封的类本身(该类密封后是不可继承的),删除了惰性初始化代码,删除了Instance()方法,并且对_instance变量做了大量的修改。对_instance变量所做的更改包括修改对公共方法的访问级别,将变量标记为只读,以及在声明时初始化该变量。此处,我们可以直接定义所需的行为,而不关心实现的潜在有害的副作用。那么,使用惰性初始化有什么优点以及使用多个线程有什么危险呢?在.NET框架中内置了所有正确的行为。让我们先看第一种情况:惰性初始化。
最初使用惰性初始化的主要原因是要获取仅在第一次调用Instance()方法中创建实例的行为,还因为C++规范中具有某种开放性,并不定义静态变量的确切初始化顺序。要在C++中获得所需的Singleton行为,必须采用涉及使用惰性初始化的运算方法。我们真正关心的是在第一次(在该情况下)调用实例属性中创建该实例,还是在此调用之前创建该实例的,并且类中的静态变量是否有已定义的初始化顺序。对于.NET框架,这就是我们获取的行为。在JIT过程中,当(且仅当)任何方法使用静态属性时,“框架”将初始化此静态属性。如果没有使用该属性,则不会创建实例。更准确地说,在JIT过程中发生的事情就是,在任何调用方使用该类的任何静态成员时构造和加载该类。在这种情况下,结果是相同的。
那么,线程安全初始化呢?“框架”也解决了这一问题。“框架”内部保证静态类型初始化的线程安全。换句话说,在上面的示例中,只创建一个Singleton类实例。还要注意,用于保存类实例的属性字段称为实例。此选项更好地说明了,在本文中的讨论过程中,此值是类的实例。在“框架”本身中,虽然使用的属性名称称为值,但有多个类使用此类型的Singleton。概念完全相同。
对类所做的其他更改意味着禁止划分子类。添加密封类修饰符可确保不会将该类划分为子类。GoFSingleton模式详细介绍了试图对Singleton划分子类所产生的问题,该划分通常并不是小事。在大多数情况下,可以很容易地开发没有父类的Singleton,并且添加划分子类功能会增加通常根本不需要的新的复杂性级别。随着复杂性的提高,测试、培训和文档编制等所需的时间也会增加。通常,除非绝对必要,否则您不希望提高任何代码的复杂性。
让我们看一下如何使用Singleton。使用我们最初的计数器的有关动机的概念,我们可以创建一个简单的Singleton计数器类并说明我们将如何使用它。图3显示了UML类说明将包含什么内容。
相应的类实现代码以及示例客户端使用如下所示。
示例Singleton使用

sealed class SingletonCounter {
public static readonly SingletonCounter Instance =
new SingletonCounter();
private long Count = 0;
private SingletonCounter() {}
public long NextValue() {
return ++Count;
}
}

class SingletonClient {
[STAThread]
static void Main() {
for (int i=0; i<20; i++) {
Console.WriteLine("NextSingletonvalue: {0}",
SingletonCounter.Instance.NextValue());
}
}
}

此处,我们还创建了一个Singleton类来维护具有long类型的增量计数。客户端是一个简单的控制台应用程序,它显示计数器类的20个值。虽然此示例极其简单,但它却说明了如何使用.NET来实现Singleton,然后将其用在应用程序中。

小结
Singleton设计模式是一个非常有用的机制,可用于在面向对象的应用程序中提供单个对象访问点。无论使用的是什么实现,该模式提供一个大家所熟知的概念,以便其在设计和开发小组之间方便地进行共享。但是,正如我们所发现的一样,注意到这些实现有多大差异及其潜在的副作用也是非常重要的。.NET框架为模式实现者在设计所需的功能类型方面提供了很大的帮助,实现者无需处理本文中所讨论的很多副作用。在正确实现后,可以证实模式的最初目的的有效性。
设计模式是非常有用的软件设计概念,可使小组将重点放在提供最佳类型的应用程序上,而不考虑它们是什么应用程序。关键在于正确而有效地使用设计模式,目前有很多关于将设计模式用于Microsoft .NET方面的MSDN系列文档,其中介绍了如何正确而有效地使用设计模式。