[收集]单例模式(Singleton)深入研究(二):具体实现
前面章节主要涉及了一些预备的知识,从这一章起,我们将真正开始单例模式的研究
Singleton
首先看一个最常见的单例模式的实现,也是很多人常用的一种方式:
Singleton 设计模式的下列实现采用了 Design Patterns: Elements of Reusable Object-Oriented Software[Gamma95]
中所描述的解决方案,但对它进行了修改,以便利用 C#中可用的语言功能,如属性:
版本1:非线程安全的实现
// Bad Code ! Do not use!
public sealed class Singleton
{
static Singleton instance=null;
Singleton()
{
}
public static Singleton Instance
{
get
{
if (instance==null)
{
instance = new Singleton();
}
return instance;
}
}
}
该实现主要有两个优点:
• 由于实例是在 Instance属性方法内部创建的,因此类可以使用附加功能(例如,对子类进行实例化),即使它可能引入
不想要的依赖性。
• 直到对象要求产生一个实例才执行实例化;这种方法称为"懒实例化"。懒实例化避免了在应用程序启动时实例化不必要的
singleton。
但是,这种实现的主要缺点是在多线程环境下它是不安全的。
如果执行过程的不同线程同时判断if(instance==null)语句,发现instance为null,那就可能会创建多个 Singleton对象实例。
解决此问题的方法有很多。
版本二:线程安全简洁版
public sealed class Singleton
{
static Singleton instance=null;
static readonly object padlock = new object();
Singleton()
{
}
public static Singleton Instance
{
get
{
lock (padlock)
{
if (instance==null)
{
instance = new Singleton();
}
return instance;
}
}
}
}
在第二个版本中,我们做到了线程安全。同时我们也实现了惰性加载机制。
这个版本的唯一不足之处是lock可能会对大量的并发访问的效率造成影响。但一般的应用中这样的效率损失可以忽略不计
如果访问的效率对我们性命攸关,那我们可以改进到第三个版本
版本三:双检锁(double-check locking)保证线程安全
// Bad Code ! Do not use!
public sealed class Singleton
{
static Singleton instance=null;
static readonly object padlock = new object();
Singleton()
{
}
public static Singleton Instance
{
get
{
if (instance==null)
{ //@位置1 此位置可能有多个线程处于waitzhuangtai
lock (padlock)
{
//重复判断一次,即使在@位置1已经有多个线程涌入的情况下仍然能保证不会重复初始化Singleton
if (instance==null)
{
instance = new Singleton();
}
}
}
return instance;
}
}
}
你可以对上面的代码进行一个思考,看上去双检锁机制似乎提高了访问的效率也保证了线程安全。但其实这样的写法在很多
平台和优化编译器上是错误的。
原因在于:instance = new Singleton();这行代码在不同编译器上的行为是无法预知的。一个优化编译器可以合法地如下实现
instance = new Singleton();
1. instance = 给新的实体分配内存
2. 调用Singleton的构造函数来初始化instance的成员变量((这里的初始化指的是初始化instance的实例成员,因为Singleton
的静态成员已经初始化过了)
现在想象一下有线程A和B在调用instance,线程A先进入,在执行到步骤1的时候被踢出了cpu。然后线程B进入,B看到的是
instance已经不是null了
(内存已经分配),于是它开始放心地使用instance,但这个是错误的,因为在这一时刻,instance的成员变量还都是缺省值,
A还没有来得及执行步骤2来完成instance的初始化。
当然编译器也可以这样实现:
1. temp = 分配内存
2. 调用temp的构造函数
3. instance = temp
如果编译器的行为是这样的话我们似乎就没有问题了,但事实却不是那么简单,因为我们无法知道某个编译器具体是怎么做的。
要解决这个问题,我们可以利用 内存墙机制(MemoryBarrier)来解决这个问题,上面的代码改写如下:
版本三改正版1
public sealed class Singleton
{
static Singleton instance=null;
static readonly object padlock = new object();
Singleton()
{
}
public static Singleton Instance
{
get
{
if (instance==null)
{
lock (padlock)
{
if (instance==null)
{
Singleton newVal = new Singleton();
System.Threading.Thread.MemoryBarrier();
instance = newVal;
}
}
}
return instance;
}
}
}
版本三改正版2
public sealed class Singleton
{
static volatile Singleton instance=null;
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;
}
}
}
以上谈到的一些如多线程安全、双险锁、内存墙、等概念涉及到很多编译原理和汇编的知识,由于个人能力有限,也
没法进一步加以详细阐释,大家有兴趣的话参考一下以下一些网址吧:
http://msdn.microsoft.com/msdnmag/issues/05/10/MemoryModels/default.aspx
http://blogs.msdn.com/brada/archive/2004/05/12/130935.aspx
http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html
http://blog.joycode.com/demonfox/archive/2007/01/04/90894.aspx
http://www.cnblogs.com/dayouluo/archive/2005/12/25/304455.html
好了,到此为止,我们在第三个版本的两个改进版中同时兼顾了效率、线程安全和惰性加载。
但这两个改进版虽然从应用上没有问题,但似乎又涉及了过多的专业知识,把问题复杂化了,
让人顿生生疏之感。
既然如此,让我们再来看一个亲切一点的版本吧:
版本四:简单实现,非惰性加载
public sealed class Singleton
{
private static readonly Singleton instance = new Singleton();
private Singleton(){}
public static Singleton Instance
{
get
{
return instance;
}
}
}
这种实现简单,线程安全,缺陷主要是instance不是惰性加载的。准确的说是不一定是惰性加载的,因为我们无法得知instance
会在什么时候被初始化。若对我的描述有疑虑,请参考单例模式深入研究(一)。