C#设计模式之单例模式

介绍

单例模式(Singleton)保证一个类仅有一个实例,并提供一个访问它的全局访问点。

单例模式的结构图如下所示:

使用单例模式的原因

对一些类来说,只有一个实例是很重要的。如何才能保证一个类只有一个实例并且这个实例易于被访问呢?

基于程序员之间的约定或是利用全局变量吗?

虽然这样或许也可以实现,但是单例模式确是更好的做法。因为仅仅靠约定或是全局变量,如果重新new一个对象,还是可以创建新的实例。

经典的单例模式

现在我们先来看看经典的单例模式写法:

csharp 复制代码
  public class Singleton
  {
      private static Singleton? _instance;
      private Singleton() { }
      public static Singleton GetInstance()
      {
          if(_instance == null)
            _instance = new Singleton();
         return _instance;
      }
  }
arduino 复制代码
private static Singleton? _instance;

声明了一个静态的类变量,静态类变量在类的所有实例之间共享, 与类关联而不是与类的实例关联,它在整个应用程序域中只有一个实例。

csharp 复制代码
 private Singleton() { }

私有的构造函数,这使得我们无法通过:

ini 复制代码
 Singleton Singleton = new Singleton();

这种写法来创建它的实例,这样写会报错,如下所示:

csharp 复制代码
  public static Singleton GetInstance()
      {
          if(_instance == null)
            _instance = new Singleton();
         return _instance;
      }

该方法是获得本类实例的唯一全局访问点。

如果实例不存在,在类的内部可以通过new操作符来获取一个实例,否则返回已有的实例。

ini 复制代码
 internal class Program
 {
     static void Main(string[] args)
     {        
         Singleton singleton1 = Singleton.GetInstance();
         Singleton singleton2 = Singleton.GetInstance();
         if (singleton1 == singleton2)
             Console.WriteLine("两个对象是相同的实例");
     }
 }

比较两个对象看看它们是不是同一个实例,运行结果如下所示:

多线程下的单例模式

上述代码在多线程情况下会存在问题,如果有多个线程同时访问Singleton类调用GetInstance方法,会有可能造成创建多个实例。

查看以下代码:

arduino 复制代码
internal class Program
{
    static void Main(string[] args)
    {        
        // 创建并启动两个任务
        Task task1 = Task.Factory.StartNew(Method1);
        Task task2 = Task.Factory.StartNew(Method2);
        Task.WaitAll(task1, task2);        
        Console.WriteLine("所有任务完成");
        void Method1()
        {
            Console.WriteLine("Task 1 is running.");
            Task.Delay(1000).Wait();
            Singleton singleton1 = Singleton.GetInstance();
            Console.WriteLine(singleton1.GetHashCode());
        }
​
         void Method2()
        {         
            Console.WriteLine("Task 2 is running.");
            Task.Delay(1000).Wait();
            Singleton singleton2 = Singleton.GetInstance();
            Console.WriteLine(singleton2.GetHashCode());
        }
    }
}

运行结果如下所示:

两个对象的哈希码不同,在C#中,每个对象都有一个GetHashCode方法,该方法返回该对象的哈希码。默认情况下,GetHashCode方法是根据对象的内存地址生成的,因此两个不同的实例在内存中有不同的地址,它们的哈希码通常也是不同的。然而,这并不是说哈希码不同就一定表示两个对象是不同的。因为哈希码是一个有限的整数,存在哈希冲突的可能性。两个不同的对象可能具有相同的哈希码,这被称为哈希冲突。因此,不能仅仅通过比较哈希码就断定两个对象是相同的还是不同的。

但是我们在这里我们可以这样进行简单的判断。

那么该如何解决这个问题呢?

使用lock

C#中可以使用lock来解决。 lock 语句可确保在任何时候最多只有一个线程执行其主体。

可以这样进行改写:

csharp 复制代码
 public class Singleton
 {
     private static Singleton? _instance;
     private static readonly object _syncRoot = new object();
     private Singleton() { }
     public static Singleton GetInstance()
     {
         lock (_syncRoot)
         {
             if (_instance == null)
                 _instance = new Singleton();
         }
        return _instance;
     }
 }
csharp 复制代码
 private static readonly object _syncRoot = new object();

程序运行时创建一个静态只读的辅助对象。

csharp 复制代码
  public static Singleton GetInstance()
     {
         lock (_syncRoot)
         {
             if (_instance == null)
                 _instance = new Singleton();
         }
        return _instance;
     }

lock语句确保在任何时候最多只有一个线程执行其主体。

现在再来看看运行结果:

双重锁定

现在两个对象的哈希码一样了,在这里我们可以简单的认为是同一个实例了。

但是每次都要执行lock语句会影响性能,还需要改进:

csharp 复制代码
  public class Singleton
  {
      private static Singleton? _instance;
      private static readonly object _syncRoot = new object();
      private Singleton() { }
      public static Singleton GetInstance()
      {
          if (_instance == null)
          {
              lock (_syncRoot) 
              { 
                  if(_instance == null) 
                  {
                      _instance = new Singleton();
                  }
              }
          }               
         return _instance;
      }
  }

这样子当存在_instance时,直接返回,没有执行lock语句。为什么执行两次if(_instance == null)判断是因为当_instance == null时,如果同时有两个线程调用GetInstance方法,它们都可以通过第一次判断,由于lock的机制,这两个线程只有一个进去,另一个在外排队等候,必须等上一个进入并且退出之后,第二个线程才能进入,而此时如果没有第二层的判断,那么第一个线程与第二个线程都会创建新的实例,而添加了这个判断之后,第一个线程创建了实例,_instanc就不为空,第二个线程就无法创建新的实例了。

现在再来看看运行结果:

静态初始化

其实在实际应用中,C#也可以采用静态初始化的方法,这种方法不需要开发人员显式地编写线程安全代码,即可解决多线程环境下不安全的问题。

csharp 复制代码
 public sealed class Singleton
 {
     private static readonly Singleton _instance = new Singleton();     
     private Singleton() { }
     public static Singleton GetInstance()
     {        
       return _instance;
     }
 }

使用sealed关键字表示是密封类,阻止发生派生,因为派生可能会增加实例。

csharp 复制代码
 private static readonly Singleton _instance = new Singleton();     

在第一次引用类的任何成员时创建实例,公共语言运行时负责处理变量初始化。

现在再来看看运行结果:

由于这种静态初始化的方式是在自己被加载时就将自己实例化,所以被形象地称之为饿汉单例类 ,原先的单例模式的处理方式是要在第一次被引用时,才会将自己实例化,所以就被称为懒汉单例类

总结

本文介绍了在C#中如何使用单例模式,并介绍了在多线程模式下单例模式可能存在的问题及其解决方法,希望对你有所帮助。

参考

1、《Head First 设计模式(中文版)》

2、《大话设计模式》

3、《设计模式:可复用面向对象软件的基础》

相关推荐
机器人天才一号1 小时前
C#从入门到放弃
开发语言·c#
吾与谁归in1 小时前
【C#设计模式(10)——装饰器模式(Decorator Pattern)】
设计模式·c#·装饰器模式
冷眼Σ(-᷅_-᷄๑)8 小时前
Path.Combine容易被忽略的细节
c#·.net
SongYuLong的博客14 小时前
C# (定时器、线程)
开发语言·c#
百锦再15 小时前
详解基于C#开发Windows API的SendMessage方法的鼠标键盘消息发送
windows·c#·计算机外设
无敌最俊朗@16 小时前
unity3d————协程原理讲解
开发语言·学习·unity·c#·游戏引擎
程序设计实验室16 小时前
在网页上调起本机C#程序
c#
Crazy Struggle20 小时前
.NET 8 强大功能 IHostedService 与 BackgroundService 实战
c#·.net·.net core
fs哆哆20 小时前
C#编程:优化【性别和成绩名次】均衡分班
开发语言·c#
fathing21 小时前
c# 调用c++ 的dll 出现找不到函数入口点
开发语言·c++·c#