设计模式-1 单例模式

单例模式(Singleton Pattern)【使用频率:★★★★★】

1.概述:

确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。

 

单例指的是只能存在一个实例的类,在C#中,更准确的说法是在每个AppDomain之中只能存在一个实例的类,它是软件工程中使用最多的几种模式之一。在第一个使用者创建了这个类的实例之后,其后需要使用这个类的就只能使用之前创建的实例,无法再创建一个新的实例。

一、非线程安全

public sealed class Singleton
{
  private static Singleton instance = null;
 
  private Singleton()
  {
 
  }
 
  public static Singleton instance
  {
    get
    {
      if (instance == null)
      {
        instance = new Singleton();
      }
      return instance;
    }
  }
}

 

这种方法不是线程安全的,会存在两个线程同时执行if (instance == null)并且创建两个不同的instance,后创建的会替换掉新创建的,导致之前拿到的reference为空。

二、简单的线程安全实现

public sealed class Singleton
{
  private static Singleton instance = null;
  private static readonly object padlock = new object();
 
  private Singleton()
  {
  }
 
  public static Singleton Instance
  {
    get
    {
      lock (padlock)
      {
        if (instance == null)
        {
          instance = new Singleton();
        }
        return instance;
      }
    }
  }
}

 

相比较于实现一,这个版本加上了一个对instance的锁,在调用instance之前要先对padlock上锁,这样就避免了实现一中的线程冲突,该实现自始至终只会创建一个instance了。但是,由于每次调用Instance都会使用到锁,而调用锁的开销较大,这个实现会有一定的性能损失。

注意:这里我们使用的是新建一个private的object实例padlock来实现锁操作,而不是直接对Singleton进行上锁。直接对类型上锁会出现潜在的风险,因为这个类型是public的,所以理论上它会在任何code里调用,直接对它上锁会导致性能问题,甚至会出现死锁情况。

Note: C#中,同一个线程是可以对一个object进行多次上锁的,但是不同线程之间如果同时上锁,就可能会出现线程等待,或者严重的会出现死锁情况。因此,我们在使用lock时,尽量选择类中的私有变量上锁,这样可以避免上述情况发生。

存在问题:

问题貌似得以解决,事实并非如此。如果使用以上代码来实现单例,还是会存在单例对象不唯一。原因如下:

假如在某一瞬间线程A和线程B都在调用Instance属性,此时instance对象为null值,均能通过instance == null的判断。由于实现了lock加锁机制,线程A进入lock锁定的代码中执行实例创建代码,线程B处于排队等待状态,必须等待线程A执行完毕后才可以进入lock锁定代码。但当A执行完毕时,线程B并不知道实例已经创建,将继续创建新的实例,导致产生多个单例对象,违背单例模式的设计思想,因此需要进行进一步改进,在lock中再进行一次(instance == null)判断,这种方式称为双重检查锁定(Double-Check Locking)。

三、双重验证的线程安全实现

public sealed class Singleton
{
  private volatile static Singleton instance = null;
  private static readonly object padlock = new object();
 
  private Singleton()
  {
  }
 
  public static Singleton Instance
  {
    get
    {
      if (instance == null)
      {
        // 当第一个线程运行到这里时,此时会对locker对象 "加锁",
        // 当第二个线程运行该方法时,首先检测到locker对象为"加锁"状态,
        // 该线程就会挂起等待第一个线程解锁
        // 第一个线程运行完之后, 会对该对象"解锁"
        lock (padlock)
        {
          if (instance == null)
          {
            instance = new Singleton();
          }
        }
      }
      return instance;
    }
  } 
}

 

如果使用双重检查锁定来实现单例类,需要在静态成员变量instance之前增加修饰符volatile,被volatile修饰的成员变量可以确保多个线程都能够正确处理。

volatile的使用场景

多个线程同时访问一个变量,CLR为了效率,允许每个线程进行本地缓存,这就导致了变量的不一致性。volatile就是为了解决这个问题,volatile修饰的变量,不允许线程进行本地缓存,每个线程的读写都是直接操作在共享内存上,这就保证了变量始终具有一致性。

性能测试

为了比较这几种实现的性能,我做了一个小测试,循环拿这些实现中的单例9亿次,每次调用instance的方法执行一个count++操作,每隔一百万输出一次。

3. 模式优缺点

3.1 优点

  • 提供了对唯一实例的受控访问;
  • 由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象单例模式无疑可以提高系统的性能;
  • 可以根据实际情况需要,在单例模式的基础上扩展做出双例模式,多例模式;

3.2 缺点

  • 单例类的职责过重,里面的代码可能会过于复杂,在一定程度上违背了“单一职责原则”。
  • 如果实例化的对象长时间不被利用,会被系统认为是垃圾而被回收,这将导致对象状态的丢失。

 

4. 适用场景

  • 系统只需要一个实例对象,如系统要求提供一个唯一的序列号生成器,或者需要考虑资源消耗太大而只允许创建一个对象。
  • 客户调用类的单个实例只允许使用一个公共访问点,除了该公共访问点,不能通过其他途径访问该实例。
坚持就是力量 量变造就质变
原文地址:https://www.cnblogs.com/songxm/p/15217659.html