首页 > 如何正确的使用单例模式

如何正确的使用单例模式

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

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

  总结

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

 

 

转载于:https://www.cnblogs.com/wucj/p/3157294.html

更多相关:

  • 多线程有什么好处?提高CPU的利用率,更好地利用系统资源,使用Monitor类可以同步静态/实例化的方法的全部代码或者部分代码段,使用不同的同步类创建自己的同步机制。多线程指从软件或者硬件上实现多个线程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程提升整体处理性能。多线程是指程序中包含多个执行流,即...

  • Step1:在界面主函数的构造函数中初始化多线程 auto mythread = new QThread(); //新建connect(mythread , &QThread::finished, mythread, &QObject::deleteLater);//线程运行结束后释放内存object1->moveToThread...

  • 一、thread的基本用法 参见C++使用thread类多线程编程 。 二、类外使用多线程,访问类的成员 这几种方式,新建线程都是在类外,然后通过把友元函数或者成员函数作为thread参数。 #include #include #include using namesp...

  • 本博文是根据中科大信息学院谭立湘老师的课件加上自己的理解整理出来的 ************************************************************************************ NVIDIA在2007年推出CUDA这个统一计算架构 CUDA的基本思想是支持大量的线程级并...

  • 一、parallel communication patterns   并行通信模式 Map:映射,在特定的位置读取和写入。 Gather:收集,从多个不同的位置读入,写入一个位置。 Scatter:分发,写入多个位置。 Transpose转置 结构数组缩写为AOS,数组结构缩写为SOA 转置运算是指任务重新排序内存中的数...

  • 在.Net Framework中,配置文件一般采用的是XML格式的,.NET Framework提供了专门的ConfigurationManager来读取配置文件的内容,.net core中推荐使用json格式的配置文件,那么在.net core中该如何读取json文件呢?1、在Startup类中读取json配置文件1、使用Confi...

  •   1 public class FrameSubject extends JFrame {   2    3   …………..   4    5   //因为无法使用多重继承,这儿就只能使用对象组合的方式来引入一个   6    7   //java.util.Observerable对象了。   8    9   DateSub...

  • 本案例主要说明如何使用NSwag 工具使用桌面工具快速生成c# 客户端代码、快速的访问Web Api。 NSwagStudio 下载地址 比较强大、可以生成TypeScript、WebApi Controller、CSharp Client  1、运行WebApi项目  URL http://yourserver/swagger 然后...

  •   在绑定完Action的所有参数后,WebAPI并不会马上执行该方法,而要对参数进行验证,以保证输入的合法性.   ModelState 在ApiController中一个ModelState属性用来获取参数验证结果.   public abstract class ApiController : IHttpController,...

  • 1# 引用  C:AVEVAMarineOH12.1.SP4Aveva.ApplicationFramework.dll C:AVEVAMarineOH12.1.SP4Aveva.ApplicationFramework.Presentation.dll 2# 引用命名空间, using Aveva.Applicati...