文章目录
- 简介
- 不安全线程的单例模式
- 简单安全线程带锁
- 双重检查 - 带锁
- 安全初始化
- 安全并且懒汉式静态初始化
- 带泛型的懒汉式单例
- 异常
- 提高效率
- 总结
简介
单例模式是软件工程中广为人知的设计模式。单例模式就是指一个永远只能实例化一次。使用的方式是调用类里创建的静态方法。通常来说,单例模式创建的类,都是不带形参的 ,原因就是当创建多个实例的时候,如果参数不同的话(比如2个不同的重载构造函数),那么就会造成一些不必要的问题(如果相同的实例要被创建而且他们使用相同的参数的话,那么建议使用工厂模式),这篇文章的定位就是没有 任何的参数的情况下,通常情况下,单例模式是LAZY的,也就是说相当的容易创建。
在C#中实现单例模式有很多种方式。我将在下面以上面的目录的形式呈现给大家,开始我会跟大家介绍最常用的单例模式的写法,这些写法的线程并不安全,然后会提到懒汉式写法(Lazy-Load),然后就是线程安全,最后会跟大家介绍一下提高效率的方式。
所有的实现将会用通俗的语言来介绍,但是要注意以下:
- 只有一个构造函数,而且这个构造函数是私有的,不带任何的参数的。这是为了防止其他的类实例化这个单例类(这也许违反了设计模式),注意这种方式也会阻止子类,如果单例能被子类实例化一次,那么也可以被子类实例化多次。如果单例的每个子类都可以创建实例的话,就违反了单例模式。如果你需要父类(基类)的单例的实现的话我觉得工厂模式是可以有用到的,但是具体的类型不会被知道直到运行时(CLR)开始。
- 类是sealed的,也就是封闭的。这是没有必要的,严格意义上说,只是为了JIT提高效率。
- 静态变量 ,类型被定义为这个单例类的一个引用。
- 建立一个公共的而且静态的方法,返回值是这个单例类,如果有需要的话。
注意所有的实现都是用一个公共的且静态的属性作为实例的入口。在所有的情况下属性可以方便的转换成方法,而且和线程安全或者效率不冲突。
第一个版本 - 非线程安全
// 不要用这种方式
public sealed class Singleton
{
private static Singleton instance=null;
private Singleton()
{
}
public static Singleton Instance
{
get
{
if (instance==null)
{
instance = new Singleton();
}
return instance;
}
}
}
我要表明的是,上面的方法是非线程安全的,2个不同的线程可以同时进入这个方法,如果instance为空的并且这里返回真的情况下,都可以创建实例,这显然违反了单例模式,实际上,在测试以前,实例就已经有可能被创建了,但是内存模型不能保证这个实例能被其他的线程看到,除非合适的内存屏障已经被跨过了。
第二个版本 - 简单安全线程
public sealed class Singleton { private static Singleton instance = null; private static readonly object padlock = new object(); Singleton() { } public static Singleton Instance { get { lock (padlock) { if (instance == null) { instance = new Singleton(); } return instance; } } } }
上述实现是线程安全的。这个线程在共享的object上取出了一把锁,然后在创建实例以前检查这个实例是否被创建了。这个保护了内存屏障问题(lock保证了所有的读取操作是在LOCK获得以后发生的,所有的unlock保证了所有的写操作在lock 释放以后发生的),这样就保证了一个线程只能创建一个实例(每次只有一个线程在这段代码中运行),不巧的是,性能上来说,锁变成了每次都必须的当这个实例被响应的时候。
注意除了在锁当中锁住typeof(Singleton)这种类型的以外,我锁住了一个静态私有的变量,对于这个类来说。如果是锁 的一个对象的话,其他的类可以进入并且锁住(比如Type)这样会造成性能风险的问题甚至死锁。这是我的大体偏好 - 也许可能的话,只有锁住对象才能达到最终的目的,或者某些文章说锁是为了达到一些特别的目的。(比如等待或者脉冲一个队列)。通常来说这样的对象应该被设置成私有的。这样会让写线程安全更加的容易。
第三个版本 - 尝试线程安全(双重锁定)
public sealed class Singleton { private static Singleton instance = null; private static readonly object padlock = new object(); Singleton() { } public static Singleton Instance { get { if (instance == null) { lock (padlock) { if (instance == null) { instance = new Singleton(); } } } return instance; } } }
这个实现尝试了线程安全,当然并没必要每次都要取出lock,但是这种方式有如下4个缺点:
- 这种方式JAVA中是无效的。也许你会认为我这是在说废话,但是我觉得这是值得你去了解的。JAVA内存模型中新对象的引用被分配到实例之前并不能保证构造函数完成初始化,JAVA 内存模型重新工作(在1.5版本中),但是双重检查锁依然是坏的,在不带volatile 的变量(比如C#)。
- 不带任何的记忆屏障,在ECMA CLI规格中也是破碎的。也许在.NET 2.0以下是安全的,但是我更倾向于不依赖于哪些强类型的语义,特别的说,对于安全性来说这是值得怀疑的。如果把变量变成 volatile的是可以运行的,明确的内存屏障会进行响应,虽然后面有些情况专家都不能完全同意屏障是必须的。我打算尝试避免站在对的或者错误的立场上去回答这个问题!
- 容易报错。这种模式要像上面一样的精确 - 任何的大的改动都会造成正确性和性能方面的冲击。
- 依然没有后面说的那种写法好。
第四个版本 - 不完全lazy,但是线程安全且不用用锁
public sealed class Singleton { private static readonly Singleton instance = new Singleton(); // 显示的static 构造函数 //没必要标记类型 - 在field初始化以前 static Singleton() { } private Singleton() { } public static Singleton Instance { get { return instance; } } }
未完待续,原文链接地址:
http://csharpindepth.com/Articles/General/Singleton.aspx#unsafe