设计模式之一:单例模式(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的类型构造器
posted @ 2013-06-23 08:32  Ethan Cai  阅读(880)  评论(0编辑  收藏  举报