SingleTon模式也许是被最广泛应用的模式,但是,最近看到的几个SingleTon不得不让我出一身冷汗。
    先来看看标准的反例:
C#版
    public static SomeObject GetInstance()
    {
        
if (_instance == null)
        {
            
lock (_syncRoot)
            {
                _instance 
= new SomeObject();
            }
        }
        
return _instance;
    }
VB.Net版
    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。
    另外一种常见的错误是用双检锁的方式,这种错误的方式通常是因为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;
            }
        }
    }
    乍看上去,似乎很完美,当实例存在时,直接返回实例,不存在时去加锁,然后再判断是否存在实例(为什么?如果没有这个判断,就会和上面的例子一样,创建出多个实例),如果还是没有,那就创建实例。
    但是因为有编译器的优化,实际效果却有点不一样,假设现在有两个线程,分别是线程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之间公用一个实例,那就不能这么简单的事情了。

posted on 2007-06-13 12:23  Zhenway  阅读(1116)  评论(9编辑  收藏  举报