如何正确的使用单例模式

  在最近的一个项目里面发现好多同事喜欢这样运用单例模式,样例代码如下

public class Demo
{
  public static Demo Instance
  {
    get { return new Demo(); }
  }

  public string GetUserId()
  {
    return "001";
  }

  public string GetUserName()
  {
    return "tauruswu";
  }
}

在调用这个类的时候,是这样操作的

var id = Demo.Instance.GetUserId();
var name = Demo.Instance.GetUserName();

粗略一看,可能觉得没有问题,最开始我也是这样,看别人都这么写,我也就这么写,其实这个时候你的直觉已经明显的欺骗你了,各位看官再仔细看看Demo类里面的静态属性Instance以及我们调用的方式,有没有看出什么端倪来?

  很显然,上面的调用方法已经违背了单例模式的宗旨,或者可以说是披着单例模式的外衣,却不做单例模式该做的事情。单例模式的解释是:保证一个类仅有一个实例,并提供一个访问它的全局访问点。那么我们应该如何正确的使用单例模式了?

  何为单例模式

  再回头看上面解释单例模式的话,第一句话说“保证一个类仅有一个实例”,好,那么我们怎样能够保证一个类仅有一个实例了,幸好在C#里面,提供了私有构造器,我们在创建一个类的时候,往往会在类的构造函数里面初始化一些对象,这里的构造器是公开的,如下面

public Demo(){// to do }

那么很显然,私有构造器就是private的,如下面

private Demo(){// to do }

一旦有了私有构造器,那么这个类就会阻止类外面的代码创建实例,不相信我们就来尝试一下。还是用上面的Demo类

public class Demo
{
  private Demo(){ // to do }
}

然后再外面去实例化它,看截图

 这样一来,你应该明白了私有构造器的作用了吧。

  解释单例模式的前半句话上面说的很清楚了,然后在看看后半句“并提供一个访问它的全局访问点”,也就是说向外部提供访问该实例的方法或者属性,怎么写?我们将开篇的例子稍作修改

public class Demo
{
  private Demo()
  { 
    // to do 
  }

  private static Demo _instance;

  public static Demo Instance
  {
    get
      {
         if (_instance == null)
      {
        Console.WriteLine(string.Format("线程{0}在{1}时刻发现Instance为null", Thread.CurrentThread.Name,DateTime.Now));   _instance
= new Demo();
      }   
return _instance; }   } }

调用方式与开篇的一样,这个时候你在单步调试进去,看看发现了什么。到这里,我们因该能正确的理解单例模式以及如何使用单例模式了。

  多线程环境下莫名其妙的错误

  上面的例子在单线程环境下可以正常的运转,如果换做是多线程环境下,它还能正确的运转吗?

  我们来做这样一个实验:1. 在一个程序启动时创建两个线程,线程A与线程B

             2. 线程A与线程B分别调用Demo类

如果仅凭自觉的话,我们肯定会觉得只有一个线程来创建Demo的实例,那么事实是不是这样了?Demo类还是上面的那个类,未作任何修改。然后在另一个类中启动两个线程,分别调用类Demo

public class Invoke
    {
        public void Run()
        {
            Thread t1 = new Thread(new ThreadStart(fun1));
            t1.Name = "AAA";
            t1.Start();

            Thread t2 = new Thread(new ThreadStart(fun2));
            t2.Name = "BBB";
            t2.Start();
        }

        private void fun1()
        {
            while (true)
            {
                Demo1.Instance.GetUserId();

                Thread.Sleep(1);
            }
        }

        private void fun2()
        {
            while (true)
            {
                Demo1.Instance.GetUserId();

                Thread.Sleep(1);
            }
        }
    }

最后我们在控制台程序里面运行调用类Singleton,看效果图

哥,你目瞪口呆了吧?怎么会有这样的结果?事实证明上面的写法在多线程环境里面会出问题的,那么我们该怎么样去修改它了,让它能在多线程环境下正确的运行。

  如何修正在多线程环境下的bug

  这里我们会用到著名的双检锁技术,英文名就是“Double-Check Locking”,它是线程同步机制中的一种,它背后的思路是,如果对象已经构造好,就不需要线程同步,另外如果调用如上面提到的属性“Instance”的线程A发现对象没有创建好,就会获取一个线程同步锁来确保只有一个线程构造单例对象,基于这,我们将Demo类再稍微调整下

public class Demo1
    {
        private Demo1()
        {
            // to do 
        }

        private static Demo1 _lock = new Demo1();

        private static Demo1 _instance;

        public static Demo1 Instance
        {
            get
            {
                if (_instance != null) return _instance;

                Monitor.Enter(_lock);

                if (_instance == null)
                {
                    Console.WriteLine(string.Format("线程{0}在{1}时刻发现Instance为null", Thread.CurrentThread.Name, DateTime.Now));

                    _instance = new Demo1();
                }

                Monitor.Exit(_lock);

                return _instance;
            }
        }

    }

然后在调用类中再启动多两个线程CCC,DDD,再次启动程序

这次的结果表明只有一个线程创建了Demo类的实例了。其实上面的写法不是很严谨的,就是当私有构造器未执行完,其他的线程已经发现Instance不为null了,不过这个问题很难模拟出来。未了解决这种问题,那么就要用到Interlocked.Exchange() 这个方法。

  还有其他方式创建单例吗

  除了双检索技术,还有其他方式实现单例模式吗?答案是肯定的。先来看些下面这种方式

public class Demo2
    {
        private static Demo2 _demo2 = new Demo2();

        private Demo2()
        {
            Console.WriteLine(string.Format("线程{0}在{1}时刻执行私有构造函数", Thread.CurrentThread.Name, DateTime.Now));
        }

        public static Demo2 Instance
        {
            get { return _demo2; }
        }
    }

 

在看下执行结果图

 

那么它的原理是什么了?这里涉及到类型构造器了,由于当代码首次访问类的一个成员时,CLR 会自动调用一个类型的类构造器,所以当有一个线程访问属性Instance的时候,CLR会自动调用类构造器,从而创建这个对象的实例。

  总结

  这个话题已经被写乱了,如果我之前不仔细看项目里面的代码,我也不会发现这个问题,有些时候总是会被感觉所欺骗,所以最好的方法就是自己动手亲自实践一番,无非就是几个小时的事情而已。那么你看完这篇文章之后有没有什么感想了?

 

 

posted @ 2013-06-27 23:15  布衣人老白  阅读(1480)  评论(0编辑  收藏  举报