Ensure a class only has one instance, and provide a global point of access to.
保证一个类仅有一个实例,并提供一个访问它的全局访问点。
《设计模式:可复用面向对象软件的基础》
单件模式是几个创建型模式中最独立的一个,它的主要特点不是根据用户程序调用生产一个新的实例,而是控制某个类型的实例数量——唯一一个。很多时候我们需要在应用中保存一个唯一的实例,比如:网络端口,虽然在一台机器上一个网络端口可以被安排非常多的工作,但最终一个端口在同一时间内只能有一个进程打开。为了防止在“一窝蜂”的情况,某个进程打开后没有很好的关闭端口,导致其他人都无法使用的情况,你可以能需要用一个管理进程来控制端口,其他进程需要向这个管理进行申请服务。那么这个唯一的管理进程就是一个Singleton(单件)。
实现单件的方式很多,但大体上可以划分为两种。
实际上,Singleton模式要做的就是通过控制类型实例的创建,确保后续使用的都是之前创建好的一个实例,通过这样的一个封装,客户程序就无需知道该类型实现的内部细节。
在逻辑模型上,Singleton模式很直观。
— 类图在此 —
参与者:
Singleton自己,它的职责是定义一个Instance操作,作为客户程序访问Singleton实例的唯一入口。
public class Singleton
{
private static Singleton instance;
protected Singleton() { }
public static Singleton Instance
{
get
{
if (instance == null)
instance = new Singleton();
return instance;
}
}
}
上面这段代码使用Lazy方式创建唯一实例,大体上可以满足最初Singleton模式的设计要求,在大多数情况下这段代码也可以很好地工作。
但是在多线程环境下,这种实现方式存在很多潜在的缺陷,最直接的问题就是位于 if 部分,当多个线程几乎同时调用Singleton类的Instance静态属性的时候,instance成员可能还没有被实例化,因此它会被多个线程同时创建多次,而且最终Singleton类保存的是最后创建的那个实例。各个线程引用的对象实例不同,这违背了我们“唯一实例”的初衷。
在综合消息和线程同步的考虑后,我们可以采用一种 double check的方式来修改经典模式中的代码。
public class DoubleCheckSingleton
{
protected DoubleCheckSingleton() { }
private static volatile DoubleCheckSingleton instance = null;
public static DoubleCheckSingleton Instance()
{
if (instance == null)
{
lock (typeof(DoubleCheckSingleton))
if (instance == null)
instance = new DoubleCheckSingleton();
}
return instance;
}
}
上面的代码,有几点需要注意:
其实考虑到Singleton实例仅有一个内部静态成员保存,如果它本身的构建过程中不需要依赖其他外部机制或不需要准备很多构造参数,也可以直接用下面的定义方式。
public class CoolSingleton
{
protected CoolSingleton() { }
protected static readonly CoolSingleton instance = new CoolSingleton();
public static CoolSingleton Instance { get { return instance; } }
}
Unit Test
[TestMethod]
public void Test_CoolSingleton()
{
CoolSingleton s1 = CoolSingleton.Instance;
CoolSingleton s2 = CoolSingleton.Instance;
Assert.IsNotNull(s1);
Assert.IsNotNull(s2);
Assert.AreSame(s1, s2);
}
[TestMethod]
public void Test_CoolSingleton_ThreadSafe()
{
int threadCount = 3;
Thread[] threads = new Thread[threadCount];
Work[] works = new Work[threadCount];
for (int i = 0; i < threadCount; i++)
{
works[i] = new Work();
ThreadStart start = new ThreadStart(works[i].Procedure);
threads[i] = new Thread(start);
}
foreach (Thread th in threads) th.Start();
foreach (Thread th in threads) th.Join();
for(int i = 0; i< threadCount; i++)
for (int j = i + 1; j < threadCount; j++)
Assert.AreSame(works[i].Instance, works[j].Instance);
}
class Work
{
public CoolSingleton Instance { get; private set; }
public void Procedure()
{
Instance = CoolSingleton.Instance;
Console.WriteLine(Instance.GetHashCode());
}
}
静态变量instance在被访问之前,都会在静态构造函数中被实例化,而且它是只读的,因此一旦创建,它就不能被任何线程修改,也就不用做Double Check了。用很简短的代码就实现了一个线程安全的Singleton。很Cool。
前面我们讨论的都是线程安全的单件实现,但是在项目中我们往往需要更细粒度的单件。比如某个线程是长时间运行的后台任务,它本身存在需要模块和中间处理,它希望每个线程都有自己的单独Singleton对象,不同线程独立操作自己线程内的Singleton。
C#程序可以通过把静态成员标示为ThreadStatic,以确保它指示静态字段对于每个线程都是唯一的。
但是标识为ThreadStatic的字段,不能在静态构造函数中实例化,那么我们就不能用之前的很Cool的方式来实现,而需要退回到最经典的Lazy方式加载Singleton的方法。Lazy的方式不是线程安全的,但是我们这里实现的本身就只运行在一个线程中,这种方式反而更符合要求。
public class ThreadSingleton
{
private ThreadSingleton() { }
[ThreadStatic]
private static ThreadSingleton instance;
public static ThreadSingleton Instance
{
get
{
if (instance == null) instance = new ThreadSingleton();
return instance;
}
}
}
Unit Test
[TestMethod]
public void Test_ThreadSingleton()
{
int threadCount = 3;
Thread[] threads = new Thread[threadCount];
Work2[] works = new Work2[threadCount];
for (int i = 0; i < threadCount; i++)
{
works[i] = new Work2();
ThreadStart start = new ThreadStart(works[i].Procedure);
threads[i] = new Thread(start);
}
foreach (Thread th in threads) th.Start();
foreach (Thread th in threads) th.Join();
for (int i = 0; i < threadCount; i++)
for (int j = i + 1; j < threadCount; j++)
Assert.AreNotSame(works[i].Instance, works[j].Instance);
}
class Work2
{
public ThreadSingleton Instance { get; private set; }
public void Procedure()
{
Instance = ThreadSingleton.Instance;
Console.WriteLine(Instance.GetHashCode());
}
}
对于Web Form、ASP.NET、Web Service等Web类应用程序不适用ThreadStatic的方法,因为它们是在同一个IIS线程下分割的执行区域,客户端调用时传递的对象是在HttpContext中共享的。每个会话的本地全局区域不是线程,而是自己的HttpContext,因此我们需要作出少许的修改。
public class WebSingleton
{
// 足够复杂的一个 key 值,用于和HttpContext 中的其他内容相区别
private const string Key = "PracticalPattern.Generate.Singleton.WebSingleton";
private WebSingleton() {}
public static WebSingleton Instance
{
get
{
WebSingleton instance = (WebSingleton)HttpContext.Current.Items(Key);
if (instance == null)
{
instance = new WebSingleton();
HttpContext.Current.Items[Key] = instance;
}
return instance;
}
}
}
有时候,项目中需要批量的产生很多Singleton类型。我们可以考虑在类的继承关系上提供一个模板式的实现方式,毕竟实现Singleton的方法相对固定,基本过程都是踏踏实实的生成一个实例。
public interface ISingleton { }
public abstract class GenericSingleton : ISingleton
where T: ISingleton, new()
{
protected static T instance = new T();
public static T Instance { get { return instance; } }
}
public class SingletoneA : GenericSingleton, ISingleton { }
public class SingletoneB : GenericSingleton, ISingleton { }
需要注意的是,这种实现方式不是那么规范,在提供了子类快速实现的同时,在子类中对实例化的约束丢失了,用户也可以绕过Instance静态属性,直接实例化这些子类型。
Unit Test
[TestMethod]
public void Test_GenericSingleton()
{
// 使用传统Singleton方式访问,保证唯一性。
SingletoneA a1 = SingletoneA.Instance;
SingletoneA a2 = SingletoneA.Instance;
Assert.AreSame(a1, a2);
// 绕过Instance静态属性,直接实例化,破坏了唯一性。
SingletoneA a3 = new SingletoneA();
Assert.AreNotSame(a1, a3);
}
这种不太规范的做法,也是有意义的:
Singleton模式明确定义了唯一的实例数量,它的根本目的是保证某些操作的一致性,比如更新计数器,更新整个应用记得某些全局性信息。但是在使用中,也可能会因为某些操作太过于耗时,而使Singleton成为整个应用的瓶颈。
那么我们可以考虑做一个扩展,让Singleton内部可以产生N个实例,大家分担工作量,这就不至于一个人累垮而且还拖累大家。我将它称为Singleton-N模式。
public enum Status
{
Busy, // 被占用
Free
}
public interface IWorkItem
{
Status Status { get; set; }
void Deactive(); // 使用结束
}
public class WorkItemCollection
where T : class, IWorkItem
{
protected int _Max;
protected IList items;
public WorkItemCollection(int max)
{
this._Max = max;
items = new List();
}
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 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;
}
public bool CouldAddNewInstance
{
get { return items.Count < _Max; }
}
}
public class SingletonN : IWorkItem
{
private const int MaxInstance = 2; // 定义Singleton-N的这个N
private SingletonN()
{
Status = Status.Free;
}
public Status Status { get; set; }
public void Deactive()
{
this.Status = Status.Free;
}
private static WorkItemCollection collection =
new WorkItemCollection(MaxInstance);
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()
{
SingletonN s1 = SingletonN.Instance;
SingletonN s2 = SingletonN.Instance;
SingletonN s3 = SingletonN.Instance;
Assert.IsNotNull(s1);
Assert.IsNotNull(s2);
Assert.IsNull(s3); // 超出容量,未获得实例返回。
// 两个不同的实例
Assert.AreNotSame(s1, s2);
s1.Deactive();
s3 = SingletonN.Instance;
Assert.IsNotNull(s3); // 有实例被释放后,获得实例
// 获得的实例为之前创建的实例
Assert.AreSame(s1, s3);
}
从某种程度上来说,Singleton-N是ObjectPool模式的一个预备模式,它除了完成创建型模式最一般的new()外,还要负责检查和数量控制。
以上的代码中,没有加锁机制,也没有做线程完全的检查,仅仅实现了一个Singleton-N模式的功能性部分。
实际上,应用中如果出现需要使用Singleton-N的情况,往往和设计的类型间职责划分有一定关系。如果方法间没有明显的耦合情况,那么完全可以把算法提取为另外一个类,它本身就不需要Singleton;如果方法很难拆解,或者不能暴露内部结果,对共享资源的调用竞争比较激烈,那么Singleton-N就是大展身手之时。
以上素有的讨论都是限制在一个线程内的,但随着分布式应用的发展,很多关键应用都需要通过Cluster实现应用,那么麻烦就大了。因为如果应用运行在不同的服务器上,甚至跨越网络,无论用上面的那种实现方式都很难控制那个 instance == null的判断。
这个作为一个思考题,供大家考虑。我将在后续的文章中提供解决方案。