SingleTon模式也许是被最广泛应用的模式,但是,最近看到的几个SingleTon不得不让我出一身冷汗。
先来看看标准的反例:
C#版
另外一种常见的错误是用双检锁的方式,这种错误的方式通常是因为JIT编译器优化而导致的,来看一下反例:
但是因为有编译器的优化,实际效果却有点不一样,假设现在有两个线程,分别是线程A和线程B,线程A先进入Instance属性,这时实例为空,线程A依次进入Label 1、2、3、4,然后创建对象,即Label 5,这时,如果对象还没有创建完成,但是因为编译器的优化,_instance已经不再是空了,这时,线程A被弹出,线程B进入,线程B发现实例不为空,直接返回实例,但是线程B并不知道,这个实例还没有创建完成,接下来线程B就是用这个没有完成创建的实例,进行各种操作,危险性不言而喻了吧。
很多人喜欢用延迟创建SingleTon,但是,我个人觉得这种SingleTon需要对线程有一定的了解,而且,在基于.net的程序上,这个延迟似乎并不是非常重要,所以,我更倾向于最简单的方式,直接在静态字段上创建对象,这样可以轻松的规避锁和创建出多个实例,这是CLR级别保证的事情(如果CLR多次执行了,唯一的解释是,这时在多个AppDomain中执行的)。
当然,这么做的缺点就是不是足够的延迟,但是,很多场合下,这个方式已经足够的延迟了。
.net下什么时候会跑类型构造?可以肯定的回答,当第一次使用这个类型的时候。如果,这个单例类型中唯一对外公开的静态成员就是这个GetInstance方法或Instance属性,那么除了这个方法或属性被调用外,还有什么可以导致这个类型的构造被创建哪?
所以,不需要太在意延迟创建,别忘了,写的代码越多,可能会出现的Bug也越多,既然在字段上直接创建没有什么太大的问题,为什么不这么用哪?
当然,这里所说的SingleTon都是AppDomain级别的,如果要让多个AppDomain之间公用一个实例,那就不能这么简单的事情了。
先来看看标准的反例:
C#版
public static SomeObject GetInstance()
{
if (_instance == null)
{
lock (_syncRoot)
{
_instance = new SomeObject();
}
}
return _instance;
}
VB.Net版{
if (_instance == null)
{
lock (_syncRoot)
{
_instance = new SomeObject();
}
}
return _instance;
}
Public Shared Function GetInstance() As SomeObject
If _instance Is Nothing Then
SyncLock syncRoot
_instance = New SomeObject()
End SyncLock
End If
Return _instance
End Function
看到这段代码,相信大家都知道这个SingleTon的失败之处了吧,根本就是一个没有线程安全的SingleTon,用锁还用错地方了,汗水。。。假设创建SomeObject的时间比较长的话(例如访问数据库),有10个线程进来的话,创建出10个实例也是很有可能的,可以说是最失败的SingleTon。If _instance Is Nothing Then
SyncLock syncRoot
_instance = New SomeObject()
End SyncLock
End If
Return _instance
End Function
另外一种常见的错误是用双检锁的方式,这种错误的方式通常是因为JIT编译器优化而导致的,来看一下反例:
public class BadSingleTon
{
private static readonly object _syncRoot = new object();
private static BadSingleTon _instance;
private BadSingleTon()
{
// label 5
}
public static BadSingleTon Instance
{
get
{
// label 1
if (_instance == null)
{
// label 2
lock (_syncRoot)
{
// label 3
if (_instance == null)
{
// label 4
_instance = new BadSingleTon();
}
}
}
return _instance;
}
}
}
乍看上去,似乎很完美,当实例存在时,直接返回实例,不存在时去加锁,然后再判断是否存在实例(为什么?如果没有这个判断,就会和上面的例子一样,创建出多个实例),如果还是没有,那就创建实例。{
private static readonly object _syncRoot = new object();
private static BadSingleTon _instance;
private BadSingleTon()
{
// label 5
}
public static BadSingleTon Instance
{
get
{
// label 1
if (_instance == null)
{
// label 2
lock (_syncRoot)
{
// label 3
if (_instance == null)
{
// label 4
_instance = new BadSingleTon();
}
}
}
return _instance;
}
}
}
但是因为有编译器的优化,实际效果却有点不一样,假设现在有两个线程,分别是线程A和线程B,线程A先进入Instance属性,这时实例为空,线程A依次进入Label 1、2、3、4,然后创建对象,即Label 5,这时,如果对象还没有创建完成,但是因为编译器的优化,_instance已经不再是空了,这时,线程A被弹出,线程B进入,线程B发现实例不为空,直接返回实例,但是线程B并不知道,这个实例还没有创建完成,接下来线程B就是用这个没有完成创建的实例,进行各种操作,危险性不言而喻了吧。
很多人喜欢用延迟创建SingleTon,但是,我个人觉得这种SingleTon需要对线程有一定的了解,而且,在基于.net的程序上,这个延迟似乎并不是非常重要,所以,我更倾向于最简单的方式,直接在静态字段上创建对象,这样可以轻松的规避锁和创建出多个实例,这是CLR级别保证的事情(如果CLR多次执行了,唯一的解释是,这时在多个AppDomain中执行的)。
当然,这么做的缺点就是不是足够的延迟,但是,很多场合下,这个方式已经足够的延迟了。
.net下什么时候会跑类型构造?可以肯定的回答,当第一次使用这个类型的时候。如果,这个单例类型中唯一对外公开的静态成员就是这个GetInstance方法或Instance属性,那么除了这个方法或属性被调用外,还有什么可以导致这个类型的构造被创建哪?
所以,不需要太在意延迟创建,别忘了,写的代码越多,可能会出现的Bug也越多,既然在字段上直接创建没有什么太大的问题,为什么不这么用哪?
当然,这里所说的SingleTon都是AppDomain级别的,如果要让多个AppDomain之间公用一个实例,那就不能这么简单的事情了。