前言
Singleton是二十三个设计模式中比较重要也比较经常使用的模式。但是这个模式虽然简单,实现起来也会有一些小坑,让我们一起来看看吧!
实现思路
首先我们看看这个设计模式的UML类图。
很清晰的可以看到,有三点是需要我们在实现这个模式的时候注意的地方。
- 私有化的构造器
- 全局唯一的静态实例
- 能够返回全局唯一静态实例的静态方法
其中,私有化构造器是防止外部用户创建新的实例而静态方法用于返回全局唯一的静态实例供用户使用。原理清楚了,接下来我们看看一些典型的实现方式和其中的暗坑。
实现方法
最简单的实现方法
最简单的实现方法自然就是按照UML类图直接写一个类,我们看看代码。
class Program
{
static void Main(string[] args)
{
var single1 = Singleton.Instance;
var single2 = Singleton.Instance;
Console.WriteLine(object.ReferenceEquals(single1, single2));
Console.ReadLine();
}
}
class Singleton
{
private static Singleton _Instance = null;
private Singleton()
{
Console.WriteLine("Created");
}
public static Singleton Instance
{
get
{
if (_Instance == null)
{
_Instance = new Singleton();
}
return _Instance;
}
}
public void DumbMethod()
{
}
}
这段代码忠实的实现了UML类图里面的一切,查看输出结果,
证实了Singleton确实起了作用,多次调用仅仅产生了一个实例,似乎这么写就可以实现这个模式了。但是,真的会那么简单吗?
如果多线程乱入?
现在我们给刚刚的例子加点调料,假设多个对实例的调用,并不是简单的,彬彬有礼的顺序关系,二是以多线程的方式调用,那么刚刚那种实现方法,还能从容应对吗?让我们试试。把Main函数里面的调用改成这样。
static void Main(string[] args)
{
int TOTAL = 10000;
Task[] tasks = new Task[TOTAL];
for (int i = 0; i < TOTAL; i++)
{
tasks[i] = Task.Factory.StartNew(() =>
{
Singleton.Instance.DumbMethod();
});
}
Task.WaitAll(tasks);
Console.ReadLine();
}
通过Factory创造出1万个Task,几乎同时去请求这个单例,看看输出。
咦,我们刚刚写的Singleton模式失效了,这个类被创造了5次(这段代码运行多次,这个数字不一定相同),一定是多线程搞的鬼,我们刚刚写的代码没有办法应对多线程,换句话说,是非线程安全的(thread-safe),那有没有办法来攻克这个难关呢?
线程安全的单例模式
Lock版本
提到线程安全,很多同学第一反应就是用lock,不错,lock是个可行的办法,让我们试试。添加一个引用类型的对象作为lock对象,修改代码如下(什么?你问我为什必须是引用类型的对象而不能是值类型的对象?因为lock的时候,如果对象是值类型,那么lock仅仅锁住了它的一个副本,另外一个线程可以畅通无阻的再次lock,这样lock就失去了阻塞线程的意义)
private static object _SyncObj = new object();
public static Singleton Instance
{
get
{
lock (_SyncObj)
{
if (_Instance == null)
{
_Instance = new Singleton();
}
return _Instance;
}
}
}
运行一下,输出
只有一个实例创建,证明Lock起作用了,这个模式可行!不过有些不喜欢用Lock的同学可能要问,还有没有其他办法呢?答案是有的。
静态构造器版本
回想一下,C#中的类静态构造器,只会在这个类第一次被使用的时候调用一次,天然的线程安全,那我们试试不用Lock使用类静态构造器?修改Singleton类如下:
class Singleton
{
private static Singleton _Instance = null;
private Singleton()
{
Console.WriteLine("Created");
}
static Singleton()
{
_Instance = new Singleton();
}
//private static object _SyncObj = new object();
public static Singleton Instance
{
get { return _Instance; }
}
public void DumbMethod()
{
}
}
去掉了Lock,添加了一个类静态构造器,试一试。
完美!对于不喜欢用Lock(在这个例子中,实例只会创建一次但是之后的所有线程都要先排队Lock再进入Critical code进行检查,效率比较低下)的同学,类静态构造器提供了一种很好的选择。不过俗话说,人心苦不足 , 我们总是追求卓越。这个版本比Lock版本似乎更好一点,那还有没有更好的版本呢?有的。
Lazy版本
从net 4.0开始,C#开始支持延迟初始化,通过Lazy关键字,我们可以声明某个对象为仅仅当第一次使用的时候,再初始化,如果一直没有调用,那就不初始化,省去了一部分不必要的开销,提升了效率。如果你不熟悉Lazy或者想更多了解它,请参考。我们今天关注的重点在于,Lazy也是天生线程安全的,所以我们尝试用它来实现Singleton模式?修改代码如下:
class Singleton
{
private static Lazy<Singleton> _Instance = new Lazy<Singleton>(() => new Singleton());
private Singleton()
{
Console.WriteLine("Created");
}
public static Singleton Instance
{
get
{
return _Instance.Value;
}
}
public void DumbMethod()
{
}
}
输出结果中可以看到,我们达到了想要的效果:
在上面的代码中,私有变量_Instance现在是被声明为延迟初始化,这样不但天然实现了线程安全,同时在没有调用Instance静态方法的时候(也即没有调用_Instance.Value),初始化不会发生,这样就提高了效率。
总结
Singleton模式很常见,实现起来也很简单,只是要小心线程安全。以上三种方法都可以实现线程安全的Singleton模式。如果net 版本在4.0之上,建议使用Lazy版本,毕竟对比Lock版本,Lazy版本可以免去实现手动Lock之苦,对比Static版本,又有延迟初始化的性能优势,何乐而不为呢?