第4章 单件模式
4.1 经典回顾
4.2 线程安全的Singleton
4.3 细节决定成败
4.4 细颗粒度Singleton
4.5自动更新的Singleton
4.6 参数化的Singleton
4.7 跨进程的Singleton
4.8 Singleton的扩展——Singleton-N
4.9 引入配置文件管理Singleton
4.10 基于类型参数的Generic Singleton
4.11 由工厂类型协助Singleton实例管理
4.12 小结
Ensure a class only has one instance, and provide a global point of access to.
—Design Patterns : Elements of Reusable Object-Oriented Software
Singleton(单件)模式是几个创建型模式中最独立的一个,它的主要特点不是根据客户程序调用生成一个新的实例,而是控制某个类型的实例数量——唯一一个。很多时候我们需要在应用中保存一个唯一的实例,比如您公司有很多同事都需要通过MSN上网发送、接收实时消息,为了集中控制公司所有的请求都通过代理服务器的HTTP端口发送出去,虽然在那台机器的80端口上您已经安排了非常多的工作,不仅仅是MSN,还有网页浏览、B2B的Web Service调用等,但最终一个端口在同一时间内只能有一个进程打开。为了防止这种“一窝蜂”情况下,某个进程打开后没有很好关闭HTTP端口,导致其他人都无法使用的情况,您最后可能要决定用一个进程控制它,如果其他人需要服务——排队,而这个唯一进程也就是那台机器计算机软件环境中的一个Singleton(单件)。数据库中自增字段也有类似的情况,它不会因为A、B的调用在数据库不同会话中(无论是专用服务器模式还是共享服务器模式)出现A:1000、1001、1002,B:1000、1001、……9970的情况,1000~9970间的每一个号都可能出现在A或B中的某一个会话里,这时候那个自增字段的序号生成部分也是一个Singleton。
实现单件的方式有很多,但大体上可以划分为两种。
● 外部方式:客户程序在使用某些全局性(或语义上下文范围内的全局性)的对象时,做些“Try-Create-Store-Use”的工作,如果没有,就自己创建一个,但是仍把它搁在那个全局的位置上,如果原本有,就直接拿一个现成的。其代码如下:
C# 客户程序保证的Singleton方式
class Target{} class Client { private static IList<Target> list=new List<Target>(); private Target target; public void SomeMethod() { if (list.Count == 0) { target=new Target(); list.Add(target); } else target=list[0]; } public Target Instance { get { return target; } } }
Unit Test
[TestClass] public class TestClientSideSingleton { [TestMethod] public void Test() { Client client1=new Client(); client1.SomeMethod(); Client client2=new Client(); client2.SomeMethod(); Assert.AreEqual<int>(client1. Instance.GetHashCode(), client2.Instance.GetHashCode()); } }
● 内部方式:类型自己控制生成实例的数量,无论客户程序是否Try 过了,类型自己就是一个实例,客户程序使用的都是这个现成的唯一实例。
相比较而言,外部方式实在不“靠谱”,毕竟应用中的类型不止一个,即便某个类型可以恪守这种先Try的方式,仍然可能有其他类型会用到它,很难要求所有相关类型都做这种唯一性检查,最终结果就是无法唯一。内部方式把干扰因素排除在类型之外,相对更保险一些。随着集群、多处理器和多核处理器的展开,想通过简单的类型内部控制,保持其真正的Singleton越来越难,确切来说,这个单件的“单”字是有上下文和语义范围限制的。
本章除了介绍经典Singleton之外,还将根据实际技术趋势介绍如何根据不同类型生产环境实现某种语义范围的Singleton。
4.1 经典回顾
使用Singleton 模式的意图就是要保证一个类只有一个实例,同时提供客户程序一个访问它的全局访问点。进一步考虑,由于类可以被继承,因此Singleton要保证“对外”(非直接或间接继承关系)提供一个统一的访问点。最初看到这个需求您可能会直接写一个静态的全局变量,虽然声明为静态是提供全局访问的一个技巧,但它也仅解决了部分问题,没有真正限制客户程序实例化的数量。
实际上,Singleton模式要做的就是通过控制类型实例的创建,确保后续使用的都是之前创建好的一个实例,通过这样的一个封装,客户程序就无须知道该类型实现的内部细节。
在逻辑模型上,Singleton模式很直观(在整个23个模式中是最简单的了),如图4-1所示。
图4-1 单件对象的静态结构
其中的参与者就只有Singleton自己,它的职责是定义一个Instance操作(类成员或类方法、类属性),作为客户程序访问Singleton的一个,而且是唯一的一个实例。客户程序如果需要调用Singleton类型的其他实例方法,也仅可以通过Instance完成。其代码如下:
C++ 《Design Patterns : Elements of Reusable Object-Oriented Software》的描述
// 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; }
C# 把静态方法Instance()转换成静态属性后的C#描述
public class Singleton { private static Singleton instance; // 唯一实例 protected Singleton() { } // 封闭客户程序的直接实例化 public static Singleton Instance { get { if (instance == null) instance=new Singleton(); return instance; } } }
让我们回头看看这段C#代码:
● 这个Singleton类仅有一个公共类属性——Instance,区别于普通的类,Singleton类仅有一个Protected的构造函数,客户程序不可以直接实例化Singleton,即类属性 Instance就是客户程序访问“private static Singleton instance”的唯一入口。
● 其中的那个if完成的控制部分则是控制实例数量的部分,只要instance被实例化过它就不会再生成新的实例,直接把一个既有的实例反馈给客户程序。
大面上讲这段代码已经可以满足最初 Singleton 模式的设计要求,在大多数情况下那段代码也可以很好地工作。但在多线程环境下,这种实现方式存在很多潜在的缺陷,一个最直接的问题就位于if部分,当多个线程几乎同时调用Singleton类的Instance静态属性的时候,instance成员可能还没有被实例化,因此它被创建了多次,而且最终Singleton类中保存的是最后创建的那个实例,各个线程引用的对象不同,这违背了我们“唯一实例”的初衷。
这种情况往往是我们在一般调试中最头疼的事情,因为不像其他逻辑错误,可以反复重现,设计多线程应用的一个原则就是“不要臆测每个并发任务的执行次序”,但这里要模拟出两个线程同时进入,很多时候要靠多次测试中的运气。毕竟很多时候相对线程实际执行前那个等待CPU调度并被实际执行的过程而言,这里的new Singleton()可能太短暂了。
另外,对于之前从事过C++开发的同行,您可能觉得静态私有成员instance应该命名为_instance 或s_instance,虽然很长时间里笔者也经常使用那个命名方式,但本书准备全部采用《Design Guideline》的方式,如果与您的编码习惯有所冲突,请您见谅。
在综合执行效率和线程同步的考虑后,我们采用一个double check的方式修正上面的代码如下:
C# 增加Double Check后的Singleton
public class Singleton { protected Singleton() { } private static volatile Singleton instance=null; /// Lazy方式创建唯一实例的过程 public static Singleton Instance() { if (instance == null) // 外层if lock (typeof(Singleton)) // 多线程中共享资源同步 if (instance == null) // 内层if instance=new Singleton(); return instance; } }
为区别上面的代码,这里有几处是需要注意的:
● 虽然是多线程环境,但如果没有那个外层if,客户程序每次执行的时候都需要先被lock住Singleton类型,但实际在绝大多数情况下,运行起来的时候这个instance并不为空,每次都锁定Singleton类型,效率太差,这个lock很可能就成了整个应用的瓶颈。
● lock 加内层的if部分等于组成了一个相对线程安全的实例构造小环境。
● 一旦唯一的一个实例在那个线程安全的“小环境”里被创建之后,后续新发起的调用都无须经过那个lock部分,直接在外层if判断之后就可获得既有的唯一实例引用。
● 其中volatile关键字也是个要点,它表示字段可能被多个并发执行线程修改。声明为volatile的字段不受编译器优化(一般情况下默认的编译优化假定由单个线程访问)的限制,这样可以确保该字段在任何时间呈现的都是最新的值,也就是在被 lock之后,如果还没有真正完成new Singleton(),新加入的线程看到的instance都是null。
上面是经典的Singleton用C#参照C++和Java“照猫画虎”的实现,凭心而论有些繁琐。由于.NET Framework 在设计上自身就有很多类似并发控制和单实例的访问要求,所以也有很多更简洁但同时也更“靠谱”的实现方式。就像我们前面介绍的,只要保证好对外的这个Instance静态属性不变的前提,客户程序可以继续使用它。下面介绍如何基于.NET Framework对Singleton的内部实现进行改造。
4.2 线程安全的Singleton
上面的Double Check方式实现虽然基本解决了多线程情况下的Singleton问题,但从我们的经验来看,那个构造过程为什么不放到静态构造函数里?毕竟它是整个类的构造部分。一个原因是编译器可能会“好心地”把静态成员 instance的构造次序重排。C++在静态成员的构造过程中存在一些多义性,这应该也是最初《设计模式:可复用面向对象软件的基础》那本书里没有采用静态构造函数的一个原因;C#相对好很多,因为通过指定的编写方法可以明确地规定静态成员的构造顺序。考虑到Singleton实例仅由一个内部静态成员保存,如果它本身在构造过程中不需要借鉴其他外部机制或不需要准备很多构造参数,也可以直接用下述方式定义:
C#
class Singleton { private Singleton() { } public static readonly Singleton Instance=new Singleton(); }
您可能觉得这次的实现比之前的要简单得多,但它确实是多线程环境下,C#实现Singleton一个非常棒的实现方式,怎么做到的呢?
● 它省去了上面示例中那个laze构造过程,由于Instance是类的公共静态成员,因此相当于它会在类第一次被用到的时候被构造,同样的原因也就可以省去把它放在静态构造函数里的过程。
● 这里实例构造函数被彻底定义为私有的,所以客户程序和子类无法额外构造新的实例,所有的访问通过公共静态成员Instance获得唯一实例的引用,符合Singleton的设计意图。
● 至于怎么保证多线程Singleton的安全,从C#层面不好判断,我们不妨从IL层面来看看,代码如下:
IL
.class private auto ansi beforefieldinit Singleton extends [mscorlib]System.Object { .method private hidebysig specialname rtspecialname static void .cctor() cil managed { .maxstack 8 L_0000: newobj instance void Singleton::.ctor() L_0005: stsfld class Singleton Singleton::Instance L_000a: ret } … … .field public static initonly class Singleton Instance }
首先,这里有个beforefieldinit的修饰符,它告诉CLR只有当这里的静态成员(或称之为字段,Field)在静态构造函数执行后才生效,因此即便有很多线程试图引用Instance,也需要等静态构造函数执行并把静态成员Instance实例化之后才可以使用。(这里IL层面的静态构造是C#编译器隐式添加的。)
其次,虽然静态属性Instance被定义为公共的,但它是只读的(IL这里的initonly),因此一旦创建,它就不能被任何线程修改,也就不用作Double Check。
因此,很简短的C#代码就实现了一个Singleton,并且它是线程安全的。很Cool。
但这种异常简练的方式相对最经典的Lazy构造方式或Double Check方式的优势都有个适用范围,如果工程中这个类有些静态方法,它们很可能会在调用静态属性Instance之前就被客户程序用到,同时哪怕类型只有一个实例也是内存中的庞然大物,那么我们没必要在刚开始时就为其腾出很大一块地方,如果后面始终没有对其进行调用,这不就白白浪费了么?而且除非当前进程终止,否则GC几乎没有机会来回收这块“空置不用”的内存。
下面我们用这种方式来实现一个Singleton常用的情景——计数器,示例代码如下:
C#
public class Counter { private Counter() { } public static readonly Counter Instance=new Counter(); private int value; public int Next { get { return ++value; } } public void Reset() { value=0; } }
Unit Test
[TestMethod] public void Test() { Counter.Instance.Reset(); Assert.AreEqual<int>(1, Counter.Instance.Next); Assert.AreEqual<int>(2, Counter.Instance.Next); Assert.AreEqual<int>(3, Counter.Instance.Next); }
4.3 细节决定成败
上面的Singleton类型继承自System.Object,但实际项目中很多时候它都会继承自某些类型,或者实现了某些接口,这里要特别注意一些因素,因为它们会打破我们费尽心思实现的Singleton,笔者将这种情况称为“Singleton变质”。下面是最常见的两个导致“Singleton变质”的情景:
● 不要实现ICloneable接口或继承自其相关的子类,否则客户程序可以跳过已经隐蔽起来的类构造函数。下面的示例说明通过ICloneable接口的克隆过程导致私有构造函数失效,CLR通过内存结构的复制生成了一个新的实例,最终导致并非单一实例存在。
C# 会导致变质的情景
public class BaseEntity : System.ICloneable { public object Clone() //对当前实例进行克隆 { return this.MemberwiseClone(); // 例如采用这种方式克隆 } } public class Singleton : BaseEntity { // ... ... }
● 严防序列化。对于远程访问,往往需要把复杂的对象序列化后进行传递,但是序列化本身会导致Singleton特性的破坏,因为序列化事实上完成了Singleton对象的拷贝。所以不能对期望具有Singleton特性的类型声明SerializableAttribute属性。下面是一个错用情形——为Singleton类型增加了SerializableAttribute属性:
C# 会导致变质的情景
[Serializable] public class Singleton { // ... ... /// 把Singleton实例通过二进制串行化为字符串 public static string SerializeToString(Singleton graph) { MemoryStream memoryStream=new MemoryStream(); formatter.Serialize(memoryStream, graph); Byte[] arrGraph=memoryStream.ToArray(); return Convert.ToBase64String(arrGraph); } /// 通过二进制反串行化从字符串回复出Singleton实例 public static Singleton DeserializeFromString(string serializedGraph) { Byte[] arrGraph=Convert.FromBase64String(serializedGraph); MemoryStream memoryStream=new MemoryStream(arrGraph); return (Singleton)formatter.Deserialize(memoryStream); } }
4.4 细颗粒度Singleton
4.4.1 背景讨论
4.2节讨论的是线程安全的Singleton实现,但项目中我们往往需要更粗或更细颗粒度的Singleton,比如某个线程是长时间运行的后台任务,它本身存在很多模块和中间处理,但每个线程都希望有自己的线程内单独Singleton对象,其他线程也独立操作自己的线程内Singleton,这样线程级Singleton的实例总数=1(每个线程内部唯一的一个)* N(线程数)= N。
.NET程序可以通过把静态成员标示为System.ThreadStaticAttribute,以确保它指示静态字段的值对于每个线程都是唯一的,但这对于Windows Form程序很有效,对于Web Form、ASP.NET Web Service等Web类应用则不适用,因为它们是在同一个IIS线程下分割的执行区域,客户端调用时传递的对象是在HttpContext 中共享的,也就是说,它本身不可以简单地通过System.ThreadStaticAttribute实现。不仅如此,使用System.ThreadStaticAttribute也不能很潇洒地套用前面的内容写成:
C#
[ThreadStatic] public static readonly Singleton Instance=new Singleton();
按照.NET的设计要求,不要为标记该属性的字段指定初始值,因为这样的初始化只会发生一次,因此在类构造函数执行时只会影响一个线程。在不指定初始值的情况下,如果它是值类型,可依赖初始化为其默认值的字段,如果它是引用类型,则可依赖初始化为null。也就是说,在多线程情况下,除了第一个实例外,其他线程虽然也期望通过这个方式获得唯一实例,但其实获得的是一个null,不能用。
4.4.2 解决桌面应用中细颗粒度Singleton问题
对于Windows Forms,可以通过System.ThreadStaticAttribute比较容易地告诉CLR其中的静态唯一属性Instance仅在本线程内部静态,但麻烦的是如何构造它。正如上面背景介绍部分所说的,不能把它放到整个类的静态构造函数里,也不能直接初始化,那么怎么办?还好,如果这里不适用4.2节介绍的那个很cool的实现,我们就退回到最经典的那个lazy方式加载 Singleton实例的方法。您可能觉得这样的线程不安全了吧?那种实现方式确实不是线程安全的,但我们这里的Singleton构造本身就已经运行在一个线程里面了,用那种不安全的方式在线程内部实现自己“一亩三分地”范围内Singleton的对象反而安全了。新的实现如下:
C#
public class Singleton { private Singleton() { } [ThreadStatic] // 说明每个Instance仅在当前线程内为静态 private static Singleton instance; public static Singleton Instance { get { if (instance == null) instance=new Singleton(); return instance; } } }
Unit Test
/// 每个线程需要执行的目标对象定义 /// 同时在它内部完成线程内部是否Singleton的情况 class Work { public static IList<int> Log=new List<int>(); /// 每个线程的执行部分定义 public void Procedure() { Singleton s1=Singleton.Instance; Singleton s2=Singleton.Instance; // 证明可以正常构造实例 Assert.IsNotNull(s1); Assert.IsNotNull(s2); // 验证当前线程执行体内部两次引用的是否为同一个实例 Assert.AreEqual<int>(s1.GetHashCode(), s2.GetHashCode()); //登记当前线程所使用的Singleton对象标识 Log.Add(s1.GetHashCode()); } } [TestClass] public class TestSingleton { private const int ThreadCount=3; [TestMethod] public void Test() { // 创建一定数量的线程执行体 Thread[] threads=new Thread[ThreadCount]; for (int i=0; i < ThreadCount; i++) { ThreadStart work=new ThreadStart((new Work()).Procedure); threads[i]=new Thread(work); } // 执行线程 foreach (Thread thread in threads) thread.Start(); // 终止线程并作其他清理工作 // ... ... // 判断是否不同线程内部的Singleton实例是不同的 for (int i=0; i < ThreadCount - 1; i++) for (int j=i+1; j < ThreadCount; j++) Assert.AreNotEqual<int>(Work.Log[i], Work.Log[j]); } }
下面我们分析单元测试代码说明的问题:
● 在Work.Procedure()方法中,两次调用了Singleton类的Instance静态属性,经过验证是同一个Singleton类实例。同时由于Singleton类的构造函数定义为私有,所以线程(客户程序)无法自己实例化Singleton类,因此也实现了该模式的设计意图。
● 通过对每个线程内部使用的Singleton实例登记并检查,确认不同线程内部其实掌握的是不同实例的引用,因此满足我们需要实现的细颗粒度(线程级)的需求。
4.4.3 解决Web应用中细颗粒度Singleton问题
上面用ThreadStatic虽然解决了Windows Form的问题,但对于Web Form应用而言并不适用,原因是Web Form应用中每个会话的本地全局区域不是线程,而是自己的HttpContext,因此,相应的Singleton实例也应该被保存在这个位置。实现上我们只需要做少许的修改,就可以完成一个Web Form下的细颗粒度Singleton设计:
这里的Web Form应用包括ASP.NET Application、ASP.NET Web Service、ASP.NET AJAX等相关应用。但示例并没有在.NET Compact Framework和.NET Micro Framework的环境下进行过验证。
C#
public class Singleton { /// <summary> /// 足够复杂的一个key值,用于和HttpContext中的其他内容相区别 /// </summary> private const string Key="marvellousWorks.practical.singleton"; private Singleton() { } public static Singleton Instance { get { // 基于HttpContext的Lazy实例化过程 Singleton instance=(Singleton)HttpContext.Current.Items[Key]; if (instance == null) { instance=new Singleton(); HttpContext.Current.Items[Key]=instance; } return instance; } } }
Unit Test
using System; using System.Web; using MarvellousWorks.PracticalPattern.SingletonPattern.WebContext; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace SingletonPattern.Test.Web { public partial class _Default : System.Web.UI.Page { protected void Page_Load(object sender, EventArgs e) { Singleton s1=Singleton.Instance; Singleton s2=Singleton.Instance; // 确认获得的Singleton实例引用确实已经被实例化了 Assert.IsNotNull(s1); Assert.IsNotNull(s2); // 确认两个引用调用的是同一个Singleton实例 Assert.AreEqual<int>(s1.GetHashCode(), s2.GetHashCode()); // 显示出当前Singleton实例的标识,用于比较与其他 // HttpContext环境下的Singleton实例其实是不同的实例 instanceHashCode.Text=s1.GetHashCode().ToString(); } } }
在浏览器中的执行效果如图4-2所示。
图4-2 浏览器中的执行效果
浏览器效果
同上,这段单元测试验证了Web Form下的细颗粒度Singleton,将唯一实例的存储位置从当前线程迁移到HttpContext,一样可以实现细颗粒度的Singleton设计意图。
4.4.4 更通用的细颗粒度Singleton
就像我们在第2章说的一样,如果你是一个公共库或是公共平台的设计者,您很难预料到自己的类库会运行在Windows Form还是Web Form环境下,但Singleton模式作为很多公共机制,最常用的包括计数器、时钟等,又常常会成为其他类库的基础,尤其当涉及业务领域逻辑的时候,很难在开发过程就约定死运行的模式。怎么办?
借助我们在第2章实现的公共工具,不妨作一个2 in 1的细颗粒度Singleton(听起来有点像早年的任天堂游戏卡),不过就像我们提到的面向对象设计的单一职责原则一样,把两者合并在一起会产生一些比较难看的冗余代码,不过Singleton与其他设计模式有个很显著的区别——它不太希望被外部机制实例化,因为它要保持唯一性,因此一些常用的依赖倒置技巧在这里又显得不太适用。这里实现一个稍有些冗余的Web Form+Windows Form 2 in 1的细颗粒度Singleton,如图4-3所示。
图4-3 细颗粒度单件模式的静态结构
示例代码如下:
C#
using System; using System.Web; using MarvellousWorks.PracticalPattern.Common; namespace MarvellousWorks.PracticalPattern.SingletonPattern.Combined { public class Singleton { private const string Key="marvellousWorks.practical.singleton"; private Singleton() { } //对外封闭构造 [ThreadStatic] private static Singleton instance; public static Singleton Instance { get { // 通过之前准备的GenericContext中非官方的方法 // 判断当前执行模式是Web Form还是非Web Form // 本方法没有在.NET的CF和MF 上验证过 if (GenericContext.CheckWhetherIsWeb()) // Web Form { // 基于HttpContext的Lazy实例化过程 Singleton instance = (Singleton)HttpContext.Current.Items[Key]; if (instance == null) { instance=new Singleton(); HttpContext.Current.Items[Key]=instance; } return instance; } else // 非Web Form方式 { if (instance == null) instance=new Singleton(); return instance; } } } } }
4.5自动更新的Singleton
似乎Singleton 从经典实现开始就一直是一个持续存在的对象,但现实中持久不变的内容还是放到数据库、XML、报文里面更好些,内存中的对象很多时候还是要被刷新的,工程中Singleton的那个唯一实例很多时候也有类似的需求。实现上更新的触发原因很多,有可能是来自外部的通知,也可能是系统内部的时钟,有时候也经常同时提供外部、内部的通知机制。
4.6 参数化的Singleton
很多时候Singleton对唯一实例的构造过程有些“莽撞”,因为要么new()一个无参数的构造函数,要么按照指定参数序列构造一个硬编码参数方式的构造函数,这确实不够灵活,即便可以把很多判断逻辑放到静态构造函数或那个Lazy构造过程,但毕竟这都是在开发过程中定义的。
C# 硬编码方式实现僵化的参数化Singleton
public class Singleton { /// 参数化构造函数 private string message; private Singleton(string message) { this.message=message; } public string Message { get { return this.message; } } /// 硬编码方式实现的参数化构造函数 private static Singleton instance; public static Singleton Instance { get { if (instance == null) lock(typeof(Singleton)) if (instance == null) if ((DateTime.Now.DayOfWeek == DayOfWeek.Sunday) || (DateTime.Now.DayOfWeek == DayOfWeek.Saturday)) instance=new Singleton("weekend"); else instance=new Singleton("work day"); return instance; } } }
既然直接硬编码太僵化,很多时候第一直觉是参考“依赖注入”的模式,把需要的参数注入到唯一实例中,如本书第1章介绍的,注入在.NET环境下有四种方式。
●构造函数方式:这种方式在Singleton行不通,因为不允许客户程序控制Singleton类型的实例化过程。
● Setter方式:通过暴露出公共实例属性(public或某些情况下internal),客户程序就可以修改唯一实例的内容,实现参数化不成问题,但同时这种方式有点太过“自由”了,无论哪里的客户程序只要可以看到相关属性就能修改,这很容易失控。这种方式备选。
●接口方式:侵入性太强,而且接口方法的实现全部都在类内部,本身就是硬编码过程,解决不了问题。
● Attributer方式:将通用的Attribte 作为桥梁,可以在外部将需要的信息注入 Singleton类型,供构造过程参考。这是可行的,但其代价相对于 Singleton 模式而言有点大。这种方式备选。
既然外部注入方式感觉上都不是很理想,还是让Singleton“内强”好了,一个不错的选择是借助访问配置系统(配置文件、ini文件或数据库等)。例如上面的那个问题,我们可以通过把message的内容配置在App.Config文件里来解决,其代码如下:
App.Config
<?xml version="1.0" encoding="utf-8" ?> <configuration> <appSettings> <add key="parameterizedSingletonMessage" value="Hello world"/> </appSettings> </configuration>
C#
public class Singleton { /// 其他处理 /// …… /// 通过访问配置实现的参数化Singleton实例构造 private static Singleton instance; public static Singleton Instance { get { if (instance == null) lock(typeof(Singleton)) if (instance == null) { /// 读取配置并进行实例化 string key="parameterizedSingletonMessage"; string message=ConfigurationManager.AppSettings[key]; instance=new Singleton(message); } return instance; } } }
UnitTest
Assert.AreEqual<string>("Hello world", Singleton.Instance.Message);
4.7 跨进程的Singleton
以上所有的讨论都是限制在一个进程内的,无论是那个很精简的线程安全实现还是细颗粒度的实现,但随着软件可用性要求的进一步提升,很多关键应用都需要通过Cluster实现应用的Scale out。这下麻烦更大了,因为之前仅仅在一个线程内部,为了实现一个线程安全的唯一实例就需要大费周折地做个Double Check或取巧地采用.NET Framework的内置机制,但如果应用运行在不同服务器,甚至跨越网络,则很难控制那个instance==null的判断。我们看看Lazy构造、Double Check和那个很精简的方式谁可以胜任:
● 经典Singleton模式中的那个Lazy构造过程:多个进程执行某些任务本身就是个并行性的工作,在单个线程或进程内部,判断instance == null可能仅仅是一瞬间的事情,但我们在上面讨论过仍然可能导致两个线程一起进入这个if部分,在跨线程环境下这个问题更明显,判断自己内存中instance == null,然后再看看别人是不是也instance==null才敢实例化,这个时间相对instance=new Singleton()而言往往要长得多,因此更不“靠谱”。这个方式暂不考虑。
● Double Check方式:发现自己进程内instance==null,然后就要把自己锁起来,接着去看看别人那边是不是做好了,如果已做好,直接引用那边的;如果没做好,就让那边也先锁上,然后自己再踏踏实实地new Singleton()。听上去不错,不过别忘了,那边很可能也是这么想的。如果两边都同时这么想,您想将出现什么情况?
凭借开发经验您不难看出,图4-4里虚线的调用其实是完成不了的,但是两边又都持僵持的态度,满足一方持有部分争用资源但又试图锁定对方争用资源的典型情形,很容易形成死锁,相信您也很不愿看到这种情况的发生。
图4-4 跨进程单件模式的静态结构
● 那个精简的方式呢?还是算了吧,它压根儿就不考虑对方是否已经构造好了,直接就new Singleton()了。
考虑解决办法前,我们还是看看一般一个Cluster是怎么做的,因为如果要做负载均衡,很多时候它要记录 Cluster 中每个节点的响应情况,然后按照配置好的比例分配负载,其中一个关键的因素就是有个共享机制,也就是要有3rd(第三方)。这里也一样,当两者(甚至更多)内部都保存一个具有new Singleton()能力的类型后,常规的Singleton模式很难控制它们的实现,这时候不如直接剥夺它们new Singleton()的能力,两边作为一个代理向本进程内部的实体提供Singleton实体的引用,把构造工作抽象出去。新的设计如图4-5所示。
图4-5 增加了远程代理的单件对象静态结构
ISingleton需要部署在客户节点,构造Singleton类型节点,代理类所在节点和客户程序部分(它们可能位于同一台机器上,也可能运行在分布式网络环境下)。SingletonProxy作为每个进程内部的ISingleton访问代理,负责传达客户程序对ISingleton类型的调用请求。至于那个真正的Singleton类,则可以按部就班地套用Double Check或精简型方式,将自己锁在进程的内存空间里踏踏实实地创建并暴露出那个唯一实例。如果是Cluster环境,Singleton可能运行在Controller上,也可以运行在某个Controller+Member的机器上;如果仅仅是一台机器的多个进程间通信,它可以“寄生”在某个并行进程内部,或者自己在一个单独的进程里。
另外,还有一个半Singleton的方式,因为Singleton类型中可变的内容本质上是它的成员和它自身需要操作的数据,如果把这些内容通过某种方式固定到一个持久层,即便在每个进程内部new出一个Singleton实例,其实达到的效果是相同的。在现代的企业应用中,这部分工作往往是借助数据库数据复制或同步实现的,它相当于Singleton可变内容的后移,虽然往往可以达到类似的效果,但因为实例不唯一,所以与经典设计模式中Singleton的定义有出入,这里笔者就叫它“半”Singleton——Semi-Singleton。
4.8 Singleton的扩展——Singleton-N
Singleton模式明确定义了唯一的实例数量,但发掘其设计意图不难发现,它的根本目的是保证某些操作的一致性,比如更新计数器、更新整个应用级的某些全局性信息。就像我们可能正慢条斯理地完成一件件工作,但12月28日上司突然要求您在元旦前提交10个部门的总结,而且每份总结所需的调研时间为2天零7个小时。虽然套用以前的模板,通过填写关键业绩可以在1小时内完成10份总结,但是从总体上来说您还是来不及在元旦前提交,因为只有您一个人在有效时间内才能完成大量的工作。使用Singleton,很多时候也会导致应用出现这样的瓶颈,那么我们可以考虑作一个扩展,让Singleton内部产生N个实例,大家分担工作量,这就不至于一个人累垮而且还拖累大家。您可能会质疑,这和一般的类有什么不同?不就等于按需new()了么?其实是有些区别的,这里的N是个相对固定的数量,而且也是最多允许出现的数量。比如:N=5,那么1、2、3、4个实例都是可以的,5也可以,但当5个实例都处于忙碌状态、再次进行new()的时候,系统就会告诉您“超出服务容量了”。作为一种扩展,这里将它称为Singleton-N模式。
您可能注意到了上面描述的几个关键点:
● 最多N。
● “如果处于忙碌状态”。
● 按需new()。
其实,从某种程度上来说,Singleton-N是ObjectPool模式的一个预备模式,它除了完成创建型模式最一般的new()之外,还要负责检查和数量控制,而且往往在一个多线程环境下进行,因此,在设计上要为它增加一些“助手”。至于实现方法,经典的Lazy构造方式不适合并发环境,PASS;精简的那个方式,不适合构造多个对象,虽然它可以构造一个数组,但一句话的时候构造不了数组中的每个实例,如果把那个精简的代码展开放到静态构造函数里一个个进行实例化,又不能满足“按需new()”的性能要求,所以也PASS;最后看来,Double Check方式暂时还有改造的空间。为了不打破Double Chcek框架,并且依据单一职责原则,我们要增加一个类WorkItemCollection,它必须满足下面的条件:
● 它本身是个集合类型。
● 最多存放某种类型的N个实例。
● 集合中的每个实例都可以通过状态标示自身处于忙碌状态还是处于闲暇状态,同时可以根据外部的反馈,修正自己的状态。
● 可以告诉外界,是否还有空位,或者是否可以找出一个“热心”的闲着的对象。
● 至于new()么,WorkItemCollection还是别管了,否则容易让Singleton-N类型的构造过程失控。
为了简洁,下面的示例中没有在WorkItemCollection中增加锁机制,Singleton类型实现上也省略了Double Check,仅仅实现了一个SingletonN实例容器的功能性部分。
4.8.1 定义具有执行状态的抽象对象
定义具有执行状态的抽象对象示例代码如下:
C#
///实例的执行状况 public enum Status { Busy, // 被客户程序占用 Free // 没有被客户程序占用 } interface IWorkItem { Status Status { get; set;} void DeActivate(); // 放弃使用 }
4.8.2 定义相应的Singleton-N实例集合
定义相应的Singleton-N实例集合的示例代码如下:
C#
class WorkItemCollection<T> where T : class, IWorkItem { /// 定义最多允许保存的实例数量N protected int max; protected IList<T> items=new List<T>(); public WorkItemCollection(int max) { this.max=max; } /// 外部获得T类型实例的入口 public virtual T GetWorkItem() { if((items == null) || (items.Count == 0)) return null; // 可能的话,对外反馈一个现成实例 foreach(T item in items) if (item.Status == Status.Free) { item.Status=Status.Busy; return item; } return null; // 虽然有现成的实例,但都处于忙碌状态,所以返回null } /// 新增一个类型 public virtual void Add(T item) { if (item == null) throw new ArgumentNullException("item"); if (!CouldAddNewInstance) throw new OverflowException(); item.Status=Status.Free; // 默认状态 items.Add(item); } /// 判断是否可以增加新的实例 public virtual bool CouldAddNewInstance { get { return (items.Count < max); } } }
4.8.3 在基本Singleton模式实现的框架下引入实例集合
示例代码如下:
C#
public class SingletonN : IWorkItem { private const int MaxInstance=2; // 定义Singleton-N的这个N private Status status=Status.Free; // 初始状态 public void DeActivate() { this.status=Status.Free; } public Status Status { get { return this.status; } set { this.status=value; } } private static WorkItemCollection<SingletonN> collection = new WorkItemCollection<SingletonN>(MaxInstance); private SingletonN() { } public static SingletonN Instance { get { // 在基本实现框架不变的情况下,引入集合实现Singleton-N的多个实例对象管理 SingletonN instance=collection.GetWorkItem(); if (instance == null) if (!collection.CouldAddNewInstance) return null; else { instance=new SingletonN(); collection.Add(instance); } instance.Status=Status.Busy; // 激活使用 return instance; } } }
Unit Test
[TestMethod] public void Test() { SingletonN s1=SingletonN.Instance; SingletonN s2=SingletonN.Instance; SingletonN s3=SingletonN.Instance; Assert.IsNull(s3); // 超出容量,所以不能获得实例引用 Assert.AreNotEqual<int>(s1.GetHashCode(), s2.GetHashCode()); //两个不同实例 s1.DeActivate(); s3=SingletonN.Instance; Assert.IsNotNull(s3); //有了空间,所以可以获得引用 s2.DeActivate(); Assert.IsNotNull(s3); //有了空间,所以可以获得引用 // s3虽然获得了新的引用,但其实是之前已经创建的某个现成的 Assert.IsTrue((s3.GetHashCode() == s1.GetHashCode()) || (s3.GetHashCode() == s2.GetHashCode())); }
从上面的示例不难看出,Singleton-N相对传统Singleton模式而言,增建了多个实例的管理和执行调度,但与Object Pool不同的是它没有实现对象销毁机制,算是一个半成品。实际上应用中如果出现需要使用Singleton-N的情况往往和设计上的类型间职责划分有一定关系,如果方法间本身不存在明显的耦合情况,那么完全可以把计算上比较耗时的部分提取出新的对象,它们本身并不需要Singleton;但如果方法间本身难于拆解,或者出于安全或封装的需要,不可以对外暴露出中间结果,本身计算比较复杂,涉及调用共享争用资源的时隙又很小,这便是Singleton-N大展身手的时候了。
4.9 引入配置文件管理Singleton
上面的很多示例其实都有可通过配置完成的部分,而且工程中也需要这种生产环境的管理机制,例如:Singleton-N的这个N可能就需要按照服务器执行能力和业务负载进行调整;之前在定义“自动更新的Singleton”部分,如果采用内置时钟方式,那么最好将超时时间也写到配置里;参数化Singleton部分本身也可能把很多初始化参数写在某个配置节或配置元素集合里。
不管涉及的配置访问部分工作有多少种,建议在工程中采用或扩展.NET Framework自己的配置对象系统,这里有个关键角色 ConfigurationManager,它相当于应用和配置系统的桥梁,通过它可以直接或间接地获得.NET Framework 提供的“4 件套”配置对象(ConfigurationSectionGroup、ConfigurationSection、ConfigurationElement、Configuration-ElementCollection),一些简单的配置信息可放在AppSetting部分,如图4-6所示。
图4-6 基于配置的单件对象静态结构
4.10 基于类型参数的Generic Singleton
有时候,项目中需要批量地产生很多“不是那么规矩”的Singleton类型,也就是说,虽然内部提供了唯一的Singleton实例,但偶尔还是可以允许某些外部机制额外构造它的实例。这么做有什么意义呢?
● 首先,毕竟实现Singleton的方法相对很固定,虽然本章演绎出了很多扩展,但其实基本过程就是踏踏实实地生成一个实例,如果在类的继承关系上提供一个模板式的实现方式,很多时候可以节省客户代码;
● 其次,项目大了后很多时候就要讲规矩,比如本章访问的唯一实例全都是通过静态公共属性进行的。您可以检查之前参与的项目,称之为GetInstance()、Instance()、Singleton()的方法估计也林林总总,问题和第3章说的Factory方法一样,必要的时候可以定一个接口,并实现一个抽象基类,统一所有Singleton类型的访问点。由于事先需要部分开放子类的构造过程,因此这种Singleton有点“不那么规矩”。
● 其三,便于客户程序使用。那个唯一的实例作为静态的服务访问点,但是并不排斥客户程序从实例层面操作Singleton类的方法。(虽然实现方式不同,不过效果上您可以类比string类,它既提供静态的Equals()方法,也支持实例级Equals()方法。)
下面是这种准Singleton的实现方式代码:
C# 定义抽象部分
/// 定义一个非泛型的抽象基础 /// 否则类型约束上,只能T : class, new(),相对而言约束不够严谨。 public interface ISingleton{} public abstract class SingletonBase<T> : ISingleton where T : ISingleton, new() { protected static T instance=new T(); public static T Instance { get { return instance; } } }
C# 批量加工出一批访问点的很“规矩”的Singleton类型
/// 利用现有基础可以快速地构造出一批具有public static T Instance ///类型特征的准Singleton类型,从整体上统一Singleton方式访问的入口 class SingletonA : SingletonBase<SingletonA> { } class SingletonB : SingletonBase<SingletonB> { } class SingletonC : SingletonBase<SingletonC> { }
Unit Test
[TestMethod] public void Test() { /// 使用传统Singleton方式访问,一样可以保证唯一性 SingletonA sa1=SingletonA.Instance; SingletonA sa2=SingletonA.Instance; Assert.AreEqual<int>(sa1.GetHashCode(), sa2.GetHashCode()); /// 也可以绕过Instance静态属性,直接实例化那些准Singleton类型 SingletonA sa3=new SingletonA(); Assert.IsNotNull(sa3); Assert.AreNotEqual<int>(sa1.GetHashCode(), sa3.GetHashCode()); }
4.11 由工厂类型协助Singleton实例管理
如果按照经典方式来定义Singleton,那么客户程序无法动态生成Singleton类型实例,使用其他外部工厂类也于事无补,因为构造函数对外部封闭。但如果适当放宽一下限制,则可以通过工厂实现Concrete Singleton类型与抽象ISingleton 延迟加载,实现客户程序与Concrete Singleton的依赖倒置。比如下述情景:
● 客户程序对于各Concrete Singleton类型是通过程序集引用获得的,因此可以把各个Concrete Singleton类型与相关的Singleton Factory 定义在同一个程序集里,Concrete Singleton做到internal可见即可,这样也可达到客户程序无法直接new()的目的。例如上面的泛型Singleton示例中,SingletonA、SingletonB其实就是在.NET默认约定的访问控制符——internal中被定义的。
● 通过在Concrete Singleton类型的构造函数上增加某些安全检查,例如:权限检查属性、Role检查等,事实上也可以封闭外部客户程序的实例化过程。代码如下:
C# && Unit Test
class SingletonFactory { public ISingleton Create() { return SingletonA.Instance; } } [TestMethod] public void TestSingletonFactory() { ISingleton singleton=(new SingletonFactory()).Create(); Assert.IsNotNull(singleton); }
4.12 小结
作为创建型模式中一个很独特的模式,Singleton更多地强调可构造的类型数量和唯一实例的方式,工程上实现Singleton的挑战主要来自于它的运行环境和需要保持Singleton特质的颗粒度。随着对应用高可用性要求的提出,部署环境中Singleton往往需要在更广范围内保持其特性,如果仅通过应用的设计来解决这类问题往往又非常复杂(喧宾夺主了),为降低技术实施风险可以借助数据库、共享进程等方式近似解决分布式环境下Singleton的实现。
经典的设计模式实现很多时候都是基于许多理想化的假设,例如所有类型只有无参数的构造函数,但项目中往往不是这样的;另外,由于 Singleton的构造过程比较封闭,因此很多时候借助配置机制把参照信息提供给Singleton类型。
理想方式和项目实际往往有冲突,对于开发人员而言最直接的一个冲突恐怕就是重复编码的工作量了,稍微放宽一些Singleton类型的构造限制有时候还是可以考虑的。
最后,抛开各种讨论,我们还是重温一下Singleton模式在.NET平台一个最简洁但又相对线程安全的实现方式:
C#
class Singleton { private Singleton() { } public static readonly Singleton Instance=new Singleton(); }
您可能觉得看上去这和使用静态类几乎没有什么区别,确实有点,不过这里没有把其他实例方法写进去,同时这个类可以被继承,而不像静态类那样一步到终点,不留进一步扩展的余地。不过静态类也有自己的优势,效率相对要高一些。