单例模式深入研究

(一):从静态类和静态构造器说起

静态类和静态构造器(或叫静态构造函数)是深入理解单例模式的基础知识
一、静态类
C# 2.0 提供了静态类,在1.x中我们要实现静态类需要使用下面的代码。

1.0
public sealed class Class1
{
private Class1(){}
}


在C# 2.0中我们可以使用static class来更加优雅地解决这个问题。
public static class Class1
{
}


我们反编译一下,会了解其实现机制。

.class public abstract auto ansi sealed beforefieldinit Program
extends object
{
}


原来编译器将该类声明为 abstract sealed,自然不能被继承被实例化了。
从这段反编译的代码我们可以获知一个很重要的信息:static class机制只是编译时的处理,CLR的
运行时机制并没有因为静态类的出现而发生改变。
但是C#编译器并不允许我们在代码中直接声明一个abstract sealed类型,下面的代码无法通过编译。

public abstract sealed class Class1
{
}


静态类的限制

1. 静态类不能有实例构造器。
2. 静态类不能有任何实例成员。
3. 静态类上不能使用abstract和sealed修饰符。
4. 静态类默认继承自System.Object,不能显式指定任何其他基类。
5. 静态类不能指定任何接口实现。
6. 静态类的成员不能有protected或者protected internal访问保护修饰符。
以下是MSDN上对静态类的描述:    
当将类声明为static时,以指示它仅包含静态成员。不能使用 new 关键字创建静态类的实例。静态类在加载包含该类的程序或命名空间时由 .NET Framework 公共语言运行库 (CLR) 自动加载。
使用静态类来包含不与特定对象关联的方法。例如,创建一组不操作实例数据并且不与代码中的特定对象关联的方法是很常见的要求。您应该使用静态类来包含那些方法。
静态类的主要功能如下:

它们仅包含静态成员。

它们不能被实例化。

它们是密封的。

它们不能包含实例构造函数
关于静态类的更具体描述,请参考
http://msdn2.microsoft.com/zh-cn/library/79b3xss3(VS.80).aspx
http://www.codeproject.com/useritems/C__20_static_class.asp
    我们有些时候需要将一堆静态方法放在一个类中,而这个类又没有任何实例成员,比如 System.Web.HttpUtility 和.Net框架中的Math库就是这样的例子,由于没有任何实例成员,继承或者实例化都没有任何意义,因此静态类还是有用的。

二、静态构造器(或叫静态构造函数):
静态构造函数,可以在C#中用于初始化类数据,其方式与用于初始化实例数据的实例构造函数一样。静态构造函数与实例构造函数在使用规则上面有一些区别。与实例构造函数不一样,静态构造函数不能重载,所以可用的静态构造函数只有一个默认的无参静态构造函数(也不能添加private和public这样的关键字)。静态构造函数也不能显式的调用,不能在派生类中继承,但是在创建基类类型时可以调用。
C#在使用静态构造函数时的几个原则:
1.静态构造函数在创建类的实例之前调用,因此在所有实例构造函数之前调用。
2.静态构造函数在创建类的第一个实例之前调用。
3.静态构造函数在引用静态字段之前调用。
4.静态构造函数只能用于对静态字段的初始化。              
5.添加static关键字,不能添加访问修饰符,因为静态构造函数都是私有的。        
6.类的静态构造函数在给定应用程序域(AppDomain)中至多执行一次:只有创建类的实例或者引用类的任何静态成员才激发静态构造函数
7.静态构造函数是不可继承的,而且不能被直接调用。            
8.如果类中包含用来开始执行的 Main 方法,则该类的静态构造函数将在调用 Main 方法之前执行。    
9.任何带有初始值设定项的静态字段,则在执行该类的静态构造函数时,先要按照文本顺序执行那些初始值设定项。  


      class Test
    {
        static Test()
        {
            Console.WriteLine("a");
        }

        public Test()
        {
            Console.WriteLine("b");
        }
    }
    class Test1 : Test
    {
        public Test1()
        {
            Console.WriteLine("c");
        }
    }
实例化的时候

    Test t = new Test(); //首先调用Test的静态构造器输出a,然后调用Test的实例构造器输出b
    Test t1 = new Test();//调用Test的实例构造器输出b
    Test1 t2 = new Test1();//调用Test的实例构造器输出b,然后调用Test1的实例构造器输出c
因此最后的输出为:a,b,b,b,c
静态构造函数只调用了一次。
再来看一个示例:
在此示例中,类 Bus 有一个静态构造函数和一个静态成员 Drive()。当调用 Drive() 时,将调用静态构造函数来初始化类。
C#
复制代码
public class Bus
{
    // Static constructor:
    static Bus()
    {
        System.Console.WriteLine("The static constructor invoked.");
    }

    public static void Drive()
    {
        System.Console.WriteLine("The Drive method invoked.");
    }
}

class TestBus
{
    static void Main()
    {
        Bus.Drive();
    }
}
输出的结果如下:
The static constructor invoked.

The Drive method invoked.
要记住一点,静态构造器总是在实例构造器之前被调用。(静态类中可以存在静态构造器,但不允许存在实例构造器)
再来看一个例子:
/**************************************************
*            静 态 构 造 函 数 练 习
* (1)①②③……为执行顺序
* (2)输出结果: static A()
*                 static B()
*                 X = 1, Y = 2
***************************************************/
    class A
    {
        public static int X;
        static A()           //④ 执行完后返回到③
        {
            X = B.Y + 1;
            Console.WriteLine("static A()");
        }
    }
    class B
    {
        public static int Y = A.X + 1;      //③ 调用了A的静态成员,
        //   转到A的静态构造函数---->
        static B()           //② 如果带有初始值设定项的静态字段,
        //   执行该类的静态构造函数时,
        //   先要按照文本顺序执行那些初始值设定项。
        //   转到初始值设定项---->
        {
            Console.WriteLine("static B()");
        }
        static void Main()         //① 程序入口,
        //   如果类中包含用来开始执行的 Main 方法,
        //   该类的静态构造函数将在调用 Main 方法之前执行。
        //   转到B的静态构造函数---->
        {
            Console.WriteLine("X = {0}, Y = {1}", A.X, B.Y);//⑤ 输出结果
            Console.ReadLine();
        }
    }
关于静态构造器更详细的描述,请参考:
http://msdn2.microsoft.com/zh-cn/library/k9x6w0hc(VS.80).aspx
http://www.yaosansi.com/blog/article.asp id=730

三、前面介绍了静态类和静态构造器的基本知识,下面我们来理解一些和单例模式相关的知识
1. BeforeFieldInit标记
静态构造器(static constructors)与类型初始化器(type initializers)的关系
静态构造器的定义:
The C# specification (ECMA 334) states in section 17.11:

The static constructor for a class executes at most once in a given application domain. The execution of a static constructor is triggered by the first of the following events to occur within an application domain:
An instance of the class is created.
Any of the static members of the class are referenced.

也就是说当类被实例化或者类的静态成员被引用的时候静态构造器被调用。在同一个AppDomain中,某个类的静态构造器最多被调用一次。
再来看类型初始化器的定义:
The CLI specification (ECMA 335) states in section 8.9.5:
A type may have a type-initializer method, or not.
A type may be specified as having a relaxed semantic for its type-initializer method (for convenience below, we call this relaxed semantic BeforeFieldInit)
If marked BeforeFieldInit then the type's initializer method is executed at, or sometime before, first access to any static field defined for that type
If not marked BeforeFieldInit then that type's initializer method is executed at (i.e., is triggered by):
first access to any static or instance field of that type, or
first invocation of any static, instance or virtual method of that type

也就是说系统会有一种机制决定什么时候类型初始化器被调用(由系统自身维护的一个BeforeFieldInit来决定)
当BeforeFieldInit被标记时,类型初始化器将在首次访问这个类型的静态字段时或者之前被调用(也就是说类型初始化器被调用的时间不确定,你只能确定它调用的时间<=你首次访问类型的静态字段的时间)
当BeforeFieldInit未被标记时,类型初始化器将在首次访问该类型的成员时被调用(不论是静态成员还是实例成员)。
现在让我们来进入问题的关键:C#的定义中指出当只有在一个类型不具备静态构造器时它的BeforeFieldInit才会被自动标记上。事实上,这是由编译器帮我们完成的,它可能会导致一些我们意想不到的效果。
最后我要再次强调,静态构造器并不等同于类型初始化器。任何类型都有类型初始化器,但不一定有静态构造器
静态构造器是什么我们已经知道了,但类型初始化器到底是什么呢?
类型初始化器实际上负责帮我们初始化静态字段。假设我们定义了这样一条字段,static object o = new object();如果该字段所在的类没有静态构造器,那么o的初始化就是类型初始化器帮我们完成的,反之o的初始化由静态构造器负责。
是不是觉得很难理解呢?那我们就来看一个具体的例子吧:
class Test //没有静态构造器,BeforeFieldInit被标记
{
    static object o = new object();
}


class Test
{
    static object o;

    static Test() //有静态构造器,BeforeFieldInit未被标记
    {
        o = new object();
    }
}

你认为上面的两个类是等价的吗?

实际上以上两个类并不等价。由于第一个类没有静态构造器,因此它的BeforeFieldInit被标记了,而第二个类由于有静态构造器它的BeforeFieldInit没有被标记。因此它们的类型初始化器的调用时间并不相同。具体的调用时间请参考前面的描述。
再来看一个类
class Test
{
    static object o = new object();

    static Test()
    {
    }
}
    这个类和上面的第二个类是等价的

    为什么我们需要关注BeforeFieldInit是否被标记呢?以第三个类为例。当我们为Test添加一个空的静态构造器,它的BeforeFieldInit会被设置为未标记。这样我们就可以保证o直到该类型的字段第一次被访问或方法第一次被调用时才会被初始化,这样就可以实现惰性加载了。
   如果我们不添加这样一个空的静态构造器,我们就无法知道o什么时候被初始化(只能确定它在该类型的字段第一次被访问之前)

(二)具体实现

首先看一个最常见的单例模式的实现,也是很多人常用的一种方式:

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

会在什么时候被初始化。若对我的描述有疑虑,请参考单例模式深入研究(一)。


posted @ 2011-05-10 13:40  lifuwa  阅读(490)  评论(0编辑  收藏  举报