怪怪 | Nothing, Everything

"有过一个发疯的时刻,有感觉的钢琴以为它是世界上仅有的一架钢琴,宇宙的全部和谐都发生在它身上." - 狄德罗
随笔 - 67, 文章 - 2, 评论 - 1337, 引用 - 15
数据加载中……

对Singleton的实现方法做一个总结

原帖:

http://www.cnblogs.com/ASPNET2008/archive/2008/05/09/1190328.html

首先直接文章作者及回复中的兄弟提到的两种Singleton方法。

第一种

    public class Singleton
    
{
        
private static Singleton _instance = null;
        
private static readonly object lockHelper = new object();
        
private Singleton()
        
{
        }

        
public static Singleton CreateInstance()
        
{
            
//这样lock以及lock块内的代码只会在第一次调用CreateInstance方法的时候执行,
            
//第一次调用该方法后_instance就不再为null了,if块内的代码就无须执行了
            if (_instance == null)
            
{
                
lock (lockHelper)
                
{
                    
if (_instance == null)
                        _instance 
= new Singleton();
                }

            }

            
return _instance;
        }

    }

第二种

public class Singleton
    
{
        
private static readonly Singleton _instance = new Singleton();

        
static Singleton() { }

        
private Singleton() { }

        
public static Singleton CreateInstance() return _instance; }
    }

先说说以上两种方法。 

第一种方法, 显而易见的, 性能不佳; 而且最好加上一句Thread.MemoryBarrier()。 这是为了线性化内存存取, 避免处理器调整指令顺序导致问题, 至于到底怎么个坏掉法, 我就糊里糊涂了。 但至少从这个角度看, 其实第一种方法, 虽然我们最常用, 如果不加这么一句, 可能反而是不安全的。

 第二种方法, 有兄弟说它不是线程安全的, 事实上它是线程安全的, 但不一定是AppDomain安全的。 在同一个AppDomain内, 初始化仅进行一次。但是这种方法有一个细节。 对于没有静态构造函数的类, .NET会加上一个BeforeFieldInit的标志,它的MSDN解释是:Specifies that calling static methods of the type does not force the system to initialize the type.  这就是说, 假设Singleton存在一个静态方法, 比如Singleton.Do(), 这个Do被执行了, 但是_instance并不一定被初始化(当然也不一定不被),只要没有其它条件触发(在后面我们会想一个办法, 保证一定得到这个LazyLoad的效果); 也就是说, 初始化的时间是不固定的; 而且还有别的可能:  比如在程序集被加载, 在该类被某种方式访问, 甚至一个很随机的时刻。 传说中初始化仅仅保证在该字段被访问之前, 由运行时决定, 到底咋回事就不是我关心的范畴了。

其实早晚本来也是无所谓的, 关键是注意到这一点, 可以避免并没有触发初始化, 可是流程上人脑中有一个预期的行为,进行编程时所造成的不确定性(尽管很难想象能够掉进这个陷阱的场景,估计得特别特殊的应用, 比如你脑海里以为初始化了, 就向其它东西发一个信号, 而且会造成问题等等)。

如果存在静态构造函数, 那么当这个类的任意成员被访问前, 会进行初始化。 微软这么设计显然是有道理的: 既然写了静态构造器, 那么编程者很显然希望它最早执行, 同时也就会捎带上初始化。 但这种方式会导致, 如果咱们访问Singleton.Do(), 那么所有的字段都会被初始化。也许我们调用Do()的时候因为用不着这个实例, 而且这个实例会耗费很多资源,于是就想尽量Lazy Load用不着就不new, 这样要么使用第一种方法, 或者引出了第三个方法。

    public class Singleton
    
{
        
private Singleton() { }

        
public static Singleton CreateInstance() return Holder._instance; }

        
private class Holder
        
{
            
static Holder()
            
{
            }


            
public static readonly Singleton _instance = new Singleton();
        }

    }

用内部私有的类Holder玩一个花招, 实际上这是一种最好的办法: 当Holder被访问时, new Singleton(), 而Do()的时候, 则不会这么做。这样, 它既有第一种方法LazyLoad的特性, 又具有第二种方法的直接性和性能, 还避免了乱七八糟的很难搞明白的问题。

回头再说说关于第一种方法的另一个替代品, 这样:

public class Singleton
{
            
public static Singleton _instance = null;

            
private Singleton()
            
{

            }


            
public static Singleton CreateInstance()
            
{
                Interlocked.CompareExchange
<Singleton>(ref _instance, new Singleton(), null);
                
return _instance;
            }

}


因为Interlocked.CompareExchange是一个原子操作, 所以是线程安全的。不过注意那个new Singleton(), 它会不断的被执行。 如果构造的过程很耗费资源, 这是一个问题。 其实利用delegate, 我们可以有一些新玩法, 把包括第二次及以后进入CreateInstance的开销降到最低; 如果谁想到了可以给我留言。(Update: 我的做法放在了第20楼, 注意脑袋回复中提到的问题, 其实这种做法我一般用在别处完成更贴切的任务, 具体分析日后再说了)

最后, 稍加改动, 可以把Singleton成泛型的。不过个人感觉意义不大了, 除非用Singleton的地方太多, 懒得打代码, 也可以考虑。 嗯, 结束了么? 不知道大家觉得这些是不是也一塌糊涂,自己感觉我不太适合写这类文章....

posted on 2008-05-10 02:02 怪怪 阅读(2190) 评论(27)  编辑 收藏

评论

#1楼    回复  引用  查看    

好像又修改了一次,什么时候改的呀。
2008-05-10 06:37 | 金色海洋(jyk)      

#2楼    回复  引用  查看    

老怪物 后半夜您不睡觉啊 ^_^ 害的我一大早起来就品尝您的文章。
文中提到的第三种方法 是您自己想到滴啊?
2008-05-10 09:17 | 戏水      

#3楼    回复  引用  查看    

受小崔的影响,喜欢上这种讨论的风格了。。
2008-05-10 10:05 | 任力      

#4楼    回复  引用  查看    

这三种方法都见过,平时使用第二种,第三种很少使用。
不过使用时需要注意:既然已经创建了 Singleton.Instance 了,Do() 就没有必要再设置为 static 吧。所以使用时直接这样使用 Singleton.Instance.Do() 就可以避免初始化不确定的因素了;
第三种解决的问题是: 有了 Singleton.Instance 同时也存在 Singleton.Do(),使用 Singleton.Do() 时不能保证 mIntance 一定被初始化的问题,这样你也许就不能使用 Singleton 的成员变量。

不过看完博主比较的方法,深入地研究,是很值得学习的。辛苦了!

2008-05-10 10:21 | alonesword [未注册用户]

#5楼    回复  引用  查看    

第三种方法是通过什么确保线程安全的?
2008-05-10 10:23 | 戏水      

#6楼 [楼主]   回复  引用  查看    

@戏水
我当然没这么聪明, 印象里是从一英文站看来的, 人家玩得比我细多啦。 本来想直接转的, 一时找不到, 又考虑到语言问题, 就凭自己的使用经验重新写了。

第三种方法其实就是在类的内部玩一次带静态构造函数的第二种用法, 所以是安全的。

@alonesword
你说的对, 第二种方法一般来说足够用啦。
2008-05-10 10:35 | 怪怪      

#7楼    回复  引用  查看    

先学习
2008-05-10 11:41 | 侯垒      

#8楼    回复  引用  查看    

呵呵,Jon的《Implementing the singleton pattern》也是讲的这个,很细致。

http://www.yoda.arachsys.com/csharp/singleton.html
2008-05-10 12:33 | Klesh Wong      

#9楼 [楼主]   回复  引用  查看    

@Klesh Wong
太谢啦, 比我印象里的那篇还好, 收了 :)
2008-05-10 12:41 | 怪怪      

#10楼    回复  引用  查看    

有时候还会用到线程级别或CallContext级别的Singleton。
2008-05-10 13:15 | Jeffrey Zhao      

#11楼    回复  引用  查看    

Jeffrey Richter在他的《CLR via C#》有一节专门讲解类型构造器。而他的试验表明BeforeFieldInit元数据Flag对性能有不小的影响,加上标记比不加标记性能高出一截。So这里,我觉得不应该加空的类型构造器来抑制编译器生成BeforeFieldInit元数据Flag。当然,这里也不大可能成为性能的瓶颈,呵呵。

我主要想指出的是怪怪老大给出的采用原子操作的替代方法是不正确的,:(
所有的CAS操作必须要在一个循环内使用。虽然大多数时候,CAS操作在第一次就比较替换成功,但不保证它肯定在第一次就成功。此外,此处是否存在ABA问题也未可知,需要进行严格测试其正确性,才能使用CAS原语。详情可以参考:并发数据结构:迷人的原子
2008-05-10 13:31 | Angel Lucifer      

#12楼    回复  引用  查看    

我从来都是用第三种的
2008-05-10 14:06 | 装配脑袋      

#13楼    回复  引用  查看    

@Angel Lucifer

LZ第四种方法中,即使CompareExchange失败,也意味着其他线程将实例更新到_instance了,所以不用重试了。尽管还是有构造函数不断执行的问题……
2008-05-10 14:28 | 装配脑袋      

#14楼 [楼主]   回复  引用  查看    

@Angel Lucifer
我的看法同脑袋, 不过我的知识比较粗糙, 还要多补补课。可别叫我“老大”, 但也别叫我“老大.Next()”就行... 看你的博客对并发很有兴趣, 以后我可经常去你那看有没有新东西 :P

其实我知道Interlocked的过程是这样: 反编译.NET的DLL看它怎么用的, 我就暂时认为它的用法没问题然后克隆之; 存在疑惑的地方注意一下, 然后在今后有意识的去注意相关的知识。

缺点是在碰到补习的机会以前,有点囫囵吞枣, 还是得大家多提醒 :)

@装配脑袋
你就不用说啦, 肯定什么都找好的用 :) 你觉得我说的用delegate解决效率问题的方式可行不?
2008-05-10 17:09 | 怪怪      

#15楼    回复  引用  查看    

Mark! 如何确保所有线程当中都是唯一? 那种方式较好?
2008-05-10 17:11 | 簡簡單單..      

#16楼    回复  引用  查看    

@怪怪
我都不知道你说的delegate做法是什么……
2008-05-10 19:31 | 装配脑袋      

#17楼    回复  引用  查看    

怪怪 把 http://www.yoda.arachsys.com/csharp/singleton.html 这个文章 翻译一下吧 。造福后世啊 。哈哈
2008-05-10 19:46 | sitwo [未注册用户]

#18楼    回复  引用  查看    

@装配脑袋
是我想当然了,考虑到Singleton模式的特殊性,这样使用没有错。

@怪怪
对怪怪兄长说声抱歉。
很久没更新了,争取一个月1~2篇吧,:-)
2008-05-10 21:20 | Angel Lucifer      

#19楼    回复  引用  查看    

兽交了,LZ神人啊
晕,怎么我输入法第一个是兽交啊,应该是受教!
2008-05-10 21:27 | lovecherry      

#20楼 [楼主]   回复  引用  查看    

@簡簡單單..
我倾向于脑袋的选择, 第三种。

@sitwo
嗯我记着这事, 不过我翻译的句子比较成问题, 尤其是我和他比, 缺的全是密密麻麻的细节和分析...

@lovecherry
链接里那个才是神人呢...

P.S. 我的是第四个, 这个充分说明了一个人的纯洁性 :P

@装配脑袋
呃, 忘了说了。 是这样, 第一种和第四种方法的效率问题的解决, 可以归结为第一次进入一个过程(这个例子里是初始化), 第二次进入另一个过程(直接返回)的解决。 对这种问题, 我有时把方法换为一个委托属性。

比如

Func<Singleton> CreateInstance {
get { return _CreateInstance;}
}

_CreateInstance可能初始值如下:
delegate() {
Interlocked.CompareExchange<Singleton>(ref _instance, new Singleton(), null);
if(_instance != null) _CreateInstance = delegate() { return _instance; };
return _instance;
}

这样, 后面的调用就全都是直接返回了... 如果你觉得这种方法不会产生新的问题, 我就给更新上去...

@Angel Lucifer
嘿嘿, 那我就搬板凳去了~
2008-05-10 21:54 | 怪怪      

#21楼    回复  引用  查看    

首先,new Singleton()还是有可能多次执行,当然只有在一开始就有多个线程激烈竞争的情况下才有可能。其次一开始如果有多个线程进入CreateDelegate,他们都会执行到_CreateInstance = delegate() { return _instance; };虽然看上去没有什么问题,但与其费这么大劲,还不如用方法3……
2008-05-10 22:15 | 装配脑袋      

#22楼 [楼主]   回复  引用  查看    

@装配脑袋
对, 我也琢磨着可能会出现你说的情况, 但是考虑它只在最初热闹一会儿, 就懒得再弄得更复杂了, 就像你说的, 已经费了这么大劲了...

其实这种方式还是用在其它地方更合适...
2008-05-10 22:31 | 怪怪      

#23楼    回复  引用  查看    

无参数的话,俺倾向于第二种。:-)
2008-05-10 22:58 | Angel Lucifer      

#24楼    回复  引用  查看    

盹了,坚持看完
2008-05-12 01:41 | 镜涛      

#25楼    回复  引用  查看    

@怪怪
在第二种写法里,这句:
private static readonly Singleton _instance = new Singleton();
这种内联初始化就是使用静态的构造器(static Singleton() { })了吧。
2008-05-13 13:35 | 生鱼片      

#26楼 [楼主]   回复  引用  查看    

@生鱼片
对, 但是是不是明确的手工写上静态构造器, 会造成我上面说的区别。
2008-05-13 17:49 | 怪怪      

#27楼    回复  引用  查看    

@怪怪
o,ths
2008-05-13 18:11 | 生鱼片      

标题  
姓名  
主页
Email (只有博主才能看到) 
验证码 *  看不清,换一张
内容(请不要发表任何与政治相关的内容)  
  登录  使用高级评论  新用户注册  返回页首  恢复上次提交      
该文被作者在 2008-05-11 00:37 编辑过
 
向地震灾区捐赠爱心