zoukankan      html  css  js  c++  java
  • 如何正确的使用单例模式

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

    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会自动调用类构造器,从而创建这个对象的实例。

      总结

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

  • 相关阅读:
    ToString 格式化数值
    肾积水
    十月一日
    9月27日 星期六
    080929 气温骤降
    東京の空
    9月26日 星期五
    9月30日 星期二
    粉蓝房子&电影
    080922 雨
  • 原文地址:https://www.cnblogs.com/wucj/p/3157294.html
Copyright © 2011-2022 走看看