• 设计模式之一:单例模式(Singleton Pattern)


    写这个系列的文章,只为把所学的设计模式再系统的整理一遍。错误和不周到的地方欢迎大家批评。点击这里下载源代码。

    什么时候使用单例模式

    在程序运行时,某种类型只需要一个实例时,一般采用单例模式。为什么需要一个实例?第一,性能,第二,保持代码简洁,比如程序中通过某个配置类A读取配置文件,如果在每处使用的地方都new A(),才能读取配置项,一个是浪费系统资源(参考.NET垃圾回收机制),再者重复代码太多。

    单例模式的实现

    实现单例模式,方法非常多,这里我把见过的方式都过一遍,来体会如何在支持并发访问、性能、代码简洁程度等方面不断追求极致。(单击此处下载代码)

    实现1:非线程安全

       1: using System;
       2: using System.Collections.Generic;
       3: using System.Linq;
       4: using System.Text;
       5: using System.Threading;
       6: using System.Threading.Tasks;
       7:  
       8: namespace SingletonPatternNotTheadSafe
       9: {
      10:     public sealed class Singleton
      11:     {
      12:         private static Singleton instance = null;
      13:  
      14:         private Singleton()
      15:         {
      16:         }
      17:  
      18:         public static Singleton Instance
      19:         {
      20:             get
      21:             {
      22:                 if (instance == null)
      23:                 {
      24:                     Thread.Sleep(1000);
      25:                     instance = new Singleton();
      26:                     Console.WriteLine(string.Format(
      27:                         "[{0}]创建Singleton {1}" , Thread.CurrentThread.ManagedThreadId, instance.GetHashCode()));
      28:                 }
      29:  
      30:                 Console.WriteLine(string.Format(
      31:                         "[{0}]获得Singleton {1}", Thread.CurrentThread.ManagedThreadId, instance.GetHashCode()));
      32:                 return instance;
      33:             }
      34:         }
      35:     }
      36: }

    为了能够在下面的测试代码中展示上面代码的问题,这里在创建对象前,让线程休息1秒,并且在控制台打印出当前线程ID、对象的hashcode(一般不同对象的hashcode是不一样的,但可能重复)。

       1: using System;
       2: using System.Collections.Generic;
       3: using System.Linq;
       4: using System.Text;
       5: using System.Threading;
       6: using System.Threading.Tasks;
       7:  
       8: namespace SingletonPatternNotTheadSafe
       9: {
      10:     class Program
      11:     {
      12:         private static void Main(string[] args)
      13:         {
      14:             Thread t1 = new Thread(new ThreadStart(Compute));
      15:  
      16:             t1.Start();
      17:  
      18:             Compute();
      19:  
      20:             Console.ReadLine();  // 阻止主线程结束
      21:         }
      22:  
      23:         private static void Compute()
      24:         {
      25:             Singleton o1 = Singleton.Instance;
      26:         }
      27:     }
      28: }

    执行结果如下:

    SNAGHTML3ce09f

    分析:

    Singleton.Instance的get方法中创建instance并未考虑并发访问的情况,导致可能重复创建Singleton对象。下面的实现方法修复了此问题。

    实现2:简单线程安全

    要解决上面的问题,最简单的方法就是在创建对象的时候加锁。

       1: using System;
       2: using System.Collections.Generic;
       3: using System.Linq;
       4: using System.Text;
       5: using System.Threading;
       6: using System.Threading.Tasks;
       7:  
       8: namespace SingletonSimpleThreadSafe
       9: {
      10:     public sealed class Singleton
      11:     {
      12:         private static Singleton instance = null;
      13:         private static readonly object _lock = new object();
      14:  
      15:         private Singleton()
      16:         {
      17:         }
      18:  
      19:         public static Singleton Instance
      20:         {
      21:             get
      22:             {
      23:                 lock (_lock)
      24:                 {
      25:                     if (instance == null)
      26:                     {
      27:                         Thread.Sleep(1000);
      28:                         instance = new Singleton();
      29:                         Console.WriteLine(string.Format(
      30:                             "[{0}]创建Singleton {1}", Thread.CurrentThread.ManagedThreadId, instance.GetHashCode()));
      31:                     }
      32:                 }
      33:  
      34:                 Console.WriteLine(string.Format(
      35:                             "[{0}]获得Singleton {1}", Thread.CurrentThread.ManagedThreadId, instance.GetHashCode()));
      36:                 return instance;
      37:             }
      38:         }
      39:     }
      40: }

    测试代码如下:

       1: using System;
       2: using System.Collections.Generic;
       3: using System.Linq;
       4: using System.Text;
       5: using System.Threading;
       6: using System.Diagnostics;
       7: using System.Threading.Tasks;
       8:  
       9: namespace SingletonSimpleThreadSafe
      10: {
      11:     class Program
      12:     {
      13:         private static void Main(string[] args)
      14:         {
      15:             SingletonTest();
      16:         }
      17:  
      18:         private static void SingletonTest()
      19:         {
      20:             Thread t1 = new Thread(new ThreadStart(Compute));
      21:  
      22:             t1.Start();
      23:  
      24:             Compute();
      25:  
      26:             Console.ReadLine();  // 阻止主线程结束
      27:         }
      28:  
      29:         private static void Compute()
      30:         {
      31:             Singleton o1 = Singleton.Instance;
      32:         }
      33:     }
      34: }

    我们再看看执行效果:

    image

    创建Singleton只执行一次。但是这种写法性能并不高,每次通过Singleton.Instance获得实例对象都需要判断锁是否别别的线程占用。

    这里我们修改一下Singleton,把代码中的Thread.Sleep和Console.Writeline都去掉,这里我重新创建了一个Singleton2 class,两个线程中循环调用100000000次,看一下这么实现的性能:

       1: using System;
       2: using System.Collections.Generic;
       3: using System.Linq;
       4: using System.Text;
       5: using System.Threading;
       6: using System.Threading.Tasks;
       7:  
       8: namespace SingletonSimpleThreadSafe
       9: {
      10:     public sealed class Singleton2
      11:     {
      12:         private static Singleton2 instance = null;
      13:         private static readonly object _lock = new object();
      14:  
      15:         private Singleton2()
      16:         {
      17:         }
      18:  
      19:         public static Singleton2 Instance
      20:         {
      21:             get
      22:             {
      23:                 lock (_lock)
      24:                 {
      25:                     if (instance == null)
      26:                     {
      27:                         instance = new Singleton2();
      28:                     }
      29:                 }
      30:  
      31:                 return instance;
      32:             }
      33:         }
      34:     }
      35: }

    测试代码如下:

       1: using System;
       2: using System.Collections.Generic;
       3: using System.Linq;
       4: using System.Text;
       5: using System.Threading;
       6: using System.Diagnostics;
       7: using System.Threading.Tasks;
       8:  
       9: namespace SingletonSimpleThreadSafe
      10: {
      11:     class Program
      12:     {
      13:         private static void Main(string[] args)
      14:         {
      15:             Singleton2Test();
      16:         }
      17:  
      18:         private static void Singleton2Test()
      19:         {
      20:             Thread t1 = new Thread(new ThreadStart(Compute2));
      21:  
      22:             t1.Start();
      23:  
      24:             Compute2();
      25:  
      26:             Console.ReadLine();  // 阻止主线程结束
      27:         }
      28:  
      29:         private static void Compute2()
      30:         {
      31:             Stopwatch sw1 = new Stopwatch();
      32:  
      33:             sw1.Start();
      34:  
      35:             for (int i = 0; i < 100000000; i++)
      36:             {
      37:                 Singleton2 instance = Singleton2.Instance;
      38:             }
      39:  
      40:             sw1.Stop();
      41:  
      42:             Console.WriteLine(string.Format("[{0}]耗时:{1}毫秒", 
      43:                 Thread.CurrentThread.ManagedThreadId, 
      44:                 sw1.ElapsedMilliseconds));
      45:         }
      46:     }
      47: }

    执行结果:

    image

    我们先不讨论结果,接着往下看看双检锁方式的性能。

    实现3:双检锁实现的线程安全

    Singleton双检锁实现:

       1: using System;
       2: using System.Collections.Generic;
       3: using System.Linq;
       4: using System.Text;
       5: using System.Threading;
       6: using System.Threading.Tasks;
       7:  
       8: namespace SingletonDoubleCheckThreadSafe
       9: {
      10:     public sealed class Singleton2
      11:     {
      12:         private static Singleton2 instance = null;
      13:         private static readonly object _lock = new object();
      14:  
      15:         private Singleton2()
      16:         {
      17:         }
      18:  
      19:         public static Singleton2 Instance
      20:         {
      21:             get
      22:             {
      23:                 if (instance == null)
      24:                 {
      25:                     lock (_lock)
      26:                     {
      27:                         if (instance == null)
      28:                         {
      29:                             instance = new Singleton2();
      30:                         }
      31:                     }
      32:                 }
      33:  
      34:                 return instance;
      35:             }
      36:         }
      37:     }
      38: }

    测试代码和上面的一样,结果如下:

    image

    性能提高了(7571+7465-1410-1412)/ (7571+7465) * 100% = 81.2%。(实际项目中为了减少误差,应该跑多遍测试得到多个结果的平均值和方差,这里为了方便,我只把一次测试结果贴出来。

    双检锁机制在lock外又检查了一次instance是否为null,这样在第一次访问使instance创建后,后面的调用都无需检查lock是否被占用。

    一名程序员要了解到这里算基本合格,如果想达到更高的水平,继续往下看。这种方式有什么缺点呢?

    • 上面的代码在Java中不能正常工作。这是因为Java的Memory Model实现和.NET不一样,并不保证一定在构造函数执行完成后才返回对象的引用。虽然Java 1.5版本重构了Memory Model,但是双检锁机制在不给instance field加volatile关键字时,依然不能正常工作。
    • Microsoft的.net memory model并不是按照标准的ECMA CLI规范实现,而是在标准上做了一些“增强”工作。MS .net CLR memory model中所有的写操作都是VolatileWrite(参考《CLR via C#》第二版的第24章)。所以我们的代码中不加volatile也能在IA64CPU 架构的机器上正常执行。但是如Jeffrey建议,最好还是遵循ECMA标准。
    • 实现复杂。

    实现4:非懒加载,无锁实现线程安全

    .NET中的static变量在class被第一次实例化的时候创建,且保证仅执行一次创建。利用这个特点,可以像如下实现:

       1: using System;
       2: using System.Collections.Generic;
       3: using System.Linq;
       4: using System.Text;
       5: using System.Threading.Tasks;
       6:  
       7: namespace SingletonNotUsingLock
       8: {
       9:     public class Singleton
      10:     {
      11:         private volatile static Singleton instance = new Singleton();
      12:  
      13:         // Explicit static constructor to tell C# compiler
      14:         // not to mark type as beforefieldinit
      15:         static Singleton()
      16:         {
      17:             Console.WriteLine("execute static constructor");
      18:         }
      19:  
      20:         private Singleton()
      21:         {
      22:             Console.WriteLine("execute private constructor");
      23:         }
      24:  
      25:         public static Singleton Instance
      26:         {
      27:             get
      28:             {
      29:                 Console.WriteLine("instance get");
      30:                 return instance;
      31:             }
      32:         }
      33:     }
      34: }

    上面的代码可以更简化一些,去掉Instance属性,将私有的instance变量改成public的:

       1: using System;
       2: using System.Collections.Generic;
       3: using System.Linq;
       4: using System.Text;
       5: using System.Threading.Tasks;
       6:  
       7: namespace SingletonNotUsingLock
       8: {
       9:     public class Singleton2
      10:     {
      11:         public volatile static Singleton2 instance = new Singleton2();
      12:  
      13:         // Explicit static constructor to tell C# compiler
      14:         // not to mark type as beforefieldinit
      15:         static Singleton2()
      16:         {
      17:             Console.WriteLine("execute static constructor");
      18:         }
      19:  
      20:         private Singleton2()
      21:         {
      22:             Console.WriteLine("execute private constructor");
      23:         }
      24:     }
      25: }

    代码非常简洁。但是为什么有个静态构造函数呢,我们看看下面的测试代码:

       1: using System;
       2: using System.Collections.Generic;
       3: using System.Linq;
       4: using System.Text;
       5: using System.Threading.Tasks;
       6:  
       7: namespace SingletonNotUsingLock
       8: {
       9:     class Program
      10:     {
      11:         static void Main(string[] args)
      12:         {
      13:             Console.WriteLine("begin create singleton");
      14:  
      15:             Singleton s1 = Singleton.Instance;
      16:  
      17:             Console.WriteLine("after create singleton");
      18:  
      19:             Singleton2 s2 = Singleton2.instance;
      20:  
      21:             Console.WriteLine("after create singleton2");
      22:         }
      23:     }
      24: }

    执行结果如下:

    image

    把静态构造函数去掉后执行结果如下:

    image

    这是因为没有静态构造函数的类,编译时会被标记称beforefieldinit,那么,beforefieldinit究竟表示什么样的语义呢?Scott Allen对此进行了详细的解释:beforefieldinit为CLR提供了在任何时候执行.cctor的授权,只要该方法在第一次访问类型的静态字段之前执行即可。

    实现5:无锁懒加载

       1: using System;
       2: using System.Collections.Generic;
       3: using System.Linq;
       4: using System.Text;
       5: using System.Threading.Tasks;
       6:  
       7: namespace SingletonNotUsingLockAndLazyLoad
       8: {
       9:     public class Singleton
      10:     {
      11:         private Singleton()
      12:         {
      13:             Console.WriteLine("execute Singleton private constructor");
      14:         }
      15:  
      16:         public static Singleton Instance
      17:         {
      18:             
      19:             get
      20:             {
      21:                 Console.WriteLine("execute Singleton.Instance get");
      22:                 return Nested.instance;
      23:             }
      24:         }
      25:  
      26:         private class Nested
      27:         {
      28:             // Explicit static constructor to tell C# compiler
      29:             // not to mark type as beforefieldinit
      30:             static Nested()
      31:             {
      32:                 Console.WriteLine("execute Nested static constructor");
      33:             }
      34:  
      35:             internal static readonly Singleton instance = new Singleton();
      36:         }
      37:     }
      38: }

    实现6:使用.NET 4.0中的Lazy<T>

       1: using System;
       2: using System.Collections.Generic;
       3: using System.Linq;
       4: using System.Text;
       5: using System.Threading.Tasks;
       6:  
       7: namespace SingletonUsingLazyType
       8: {
       9:     public sealed class Singleton
      10:     {
      11:         private static readonly Lazy<Singleton> lazy =
      12:             new Lazy<Singleton>(() => new Singleton());
      13:  
      14:         public static Singleton Instance { get { return lazy.Value; } }
      15:  
      16:         private Singleton()
      17:         {
      18:         }
      19:     } 
      20: }

    参考:

    1. Exploring the Singleton Design Pattern
    2. C#设计模式(7)-Singleton Pattern
    3. Implementing the Singleton Pattern in C#
    4. c#静态构造函数
    5. C# and beforefieldinit
    6. 《研磨设计模式》
    7. 关于Type Initializer和 BeforeFieldInit的问题,看看大家能否给出正确的解释
    8. [你必须知道的.NET]第二十三回:品味细节,深入.NET的类型构造器
  • 相关阅读:
    springboot整合mybatis 异常 org.apache.ibatis.binding.BindingException: Invalid bound statement (not found):
    报时助手
    Huffman树费用
    动画效果
    工具和其他操作
    使用筛选器获取元素
    DOM操作
    属性和样式操作
    jQuery基础
    选择器
  • 原文地址:https://www.cnblogs.com/EthanCai/p/3150595.html
Copyright © 2020-2023  润新知