单件模式(C#)
2018-07-10 10:16
【原创】
本文地址:https://www.cnblogs.com/qiaoge0923/p/9282845.html
github示例代码地址:https://github.com/JoySino/DesignPatternStudy/tree/master/ConsoleDemo/ConsoleDemo/Singleton
1.前言
单件模式保证一个类只有唯一一个实例,并提供一个全局访问点。《设计模式——可复用面向对象软件的基础》中是这样定义的:
类自身保存了该类的实例对象(自己创建实例),通过截取创建新实例的请求保证没有其他新实例被创建(保证实例唯一),并且提供一个全局访问该实例对象的方法(对外提供唯一访问点),这就是Singleton模式。
常用的场景如:
系统日志记录器,系统配置管理器,模态窗口等等。
单件模式也允许子类继承,目前未接触到实际应用,暂无具体了解,所以下面的介绍均不考虑 子类扩展的情况。
2.普通懒汉单例模式
2.1代码
1 /// <summary> 2 /// 多线程安全单例模式示例 3 /// </summary> 4 public sealed class LockSingleton 5 { 6 #region 单例 7 8 private static LockSingleton _instance; 9 /// <summary> 10 /// 私有化构造函数,使得类不可通过new来创建实例 11 /// </summary> 12 private LockSingleton() { } 13 14 //写法1:通过属性获取实例 15 public static LockSingleton Instance 16 { 17 get 18 { 19 if (_instance == null) 20 { 21 _instance = new LockSingleton(); 22 } 23 return _instance; 24 } 25 } 26 27 //写法2:通过方法获取实例 28 public static LockSingleton GetInstance() 29 { 30 //这里可以copy属性Instance中的代码 31 return Instance; 32 } 33 #endregion 34 }
2.2说明
这种写法在多线程环境中会存在安全问题,两个线程同时访问时,都检测到单例对象为空,所以都会执行初始化操作,这样就会创建两个实例对象来,无法做到唯一,所以一般不建议如此写。
3.多线程安全单例模式
3.1代码
1 /// <summary> 2 /// 多线程安全单例模式示例 3 /// </summary> 4 public sealed class LockSingleton 5 { 6 #region 字段 7 8 public int normalX = SetValue(staticX); 9 10 public static int staticX = 1; 11 12 #endregion 13 14 #region 单例 15 16 private static LockSingleton _instance; 17 private static readonly object lockObj = new object(); 18 /// <summary> 19 /// 私有化构造函数,使得类不可通过new来创建实例 20 /// </summary> 21 private LockSingleton() 22 { 23 Console.WriteLine($"normalX={normalX}"); 24 Console.WriteLine($"staticX={staticX}"); 25 Console.WriteLine($"normalY={normalY}"); 26 Console.WriteLine($"staticY={staticY}"); 27 } 28 29 //写法1:通过属性获取实例 30 public static LockSingleton Instance 31 { 32 get 33 { 34 if (_instance == null) 35 { 36 lock (lockObj) 37 { 38 if (_instance == null) 39 { 40 _instance = new LockSingleton(); 41 } 42 } 43 } 44 return _instance; 45 } 46 } 47 48 //写法2:通过方法获取实例 49 public static LockSingleton GetInstance() 50 { 51 //这里可以copy属性Instance中的代码 52 return Instance; 53 } 54 #endregion 55 56 #region 其他 57 58 public int normalY = SetValue(staticY); 59 60 public static int staticY = 1; 61 62 private static int SetValue(int val) 63 { 64 return val; 65 } 66 67 #endregion 68 }
3.2调用过程
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 Console.WriteLine("程序开始"); 6 7 Singleton.LockSingleton.GetInstance(); 8 9 Console.ReadKey(); 10 } 11 }
3.3运行效果
3.4说明
normalX、staticX、normalY、staticY和Instance的执行顺序为:外部调用》staticX 》staticY》单例对象_instance》normalX》normalY》私有构造函数。
使用懒汉式单例模式时必须要考虑多线程环境下的线程安全问题,本例中直接使用了线程安全的单例模式,锁前锁后双重判断,可以有效避免多线程同时读到空单例对象可能导致创建多个实例对象的问题。
4.静态初始化单例类示例1
4.1代码
1 /// <summary> 2 /// 饿汉式单例类,静态初始化单例类示例1 3 /// </summary> 4 public sealed class ReadonlySingleton 5 { 6 #region 字段 7 8 public int normalX = SetValue(staticX); 9 10 public static int staticX = 1; 11 12 #endregion 13 14 #region 单例 15 /// <summary> 16 /// 静态初始化单例,只有通过这种写法并且调用这个字段方能是饿汉式单例 17 /// </summary> 18 public static readonly ReadonlySingleton Instance = new ReadonlySingleton(); 19 20 /// <summary> 21 /// 私有化构造函数,使得类不可通过new来创建实例 22 /// </summary> 23 private ReadonlySingleton() 24 { 25 Console.WriteLine($"normalX={normalX}"); 26 Console.WriteLine($"staticX={staticX}"); 27 Console.WriteLine($"normalY={normalY}"); 28 Console.WriteLine($"staticY={staticY}"); 29 } 30 31 #endregion 32 33 #region 其他 34 35 public int normalY = SetValue(staticY); 36 37 public static int staticY = 1; 38 39 public void ShowLog() 40 { 41 Console.WriteLine($"{Common.TimeHelper.PrintMillisecond()}:ReadonlySingleton ShowLog"); 42 } 43 44 private static int SetValue(int val) 45 { 46 return val; 47 } 48 49 #endregion 50 }
4.2调用过程
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 Console.WriteLine("程序开始"); 6 7 var tmp = Singleton.ReadonlySingleton.Instance; 8 9 Console.ReadKey(); 10 } 11 }
4.3运行效果
4.4说明
饿汉式单例类,在类加载的时候就会创建实例对象并一直存在于内存当中直至程序退出,所以,在Main函数的第一句执行前就已经执行了单例类的实例化。
normalX、staticX、normalY、staticY和Instance的执行顺序为:staticX 》Instance》normalX》normalY》构造函数》staticY》外部调用。
可以看出,对于静态字段,定义在单例字段前会先初始化,并在构造函数调用前执行,而定义在单例字段后会后初始化,并且在构造函数调用后执行;对于无修饰符的字段,无论位置在哪,都会在单例字段初始化后按顺序初始化,并且在构造函数调用前执行。所以,normalY、staticY都打印了0而不是1。
单例字段Instance是公有的,可以外部访问,只会实例化一次。这样写的单例类其字段会在类第一次使用前就初始化(具体时机由CLR决定),进而提前占用系统资源,如果想要在第一次访问时再实例化对象,那么只需要把单例字段变成私有的,并提供外部访问接口即可,或者定义内部类来实例化单例字段,详情见静态初始化单例类示例2、3、4。
5.静态初始化单例类示例2(单例私有化)
5.1代码
1 public sealed class ReadonlySingleton2 2 { 3 #region 字段 4 5 public int normalX = SetValue(staticX); 6 7 public static int staticX = 1; 8 9 #endregion 10 11 #region 单例 12 13 /// <summary> 14 /// 静态初始化单例,只有通过这种写法并且调用这个字段方能是饿汉式单例 15 /// </summary> 16 private static readonly ReadonlySingleton2 _instance = new ReadonlySingleton2(); 17 /// <summary> 18 /// 私有化构造函数,使得类不可通过new来创建实例 19 /// </summary> 20 private ReadonlySingleton2() 21 { 22 Console.WriteLine($"normalX={normalX}"); 23 Console.WriteLine($"staticX={staticX}"); 24 Console.WriteLine($"normalY={normalY}"); 25 Console.WriteLine($"staticY={staticY}"); 26 } 27 28 //写法1:通过属性获取实例 29 public static ReadonlySingleton2 Instance 30 { 31 get { return _instance; } 32 } 33 34 //写法2:通过方法获取实例 35 public static ReadonlySingleton2 GetInstance() 36 { 37 return _instance; 38 } 39 40 #endregion 41 42 #region 其他 43 44 public int normalY = SetValue(staticY); 45 public static int staticY = 1; 46 47 private static int SetValue(int val) 48 { 49 return val; 50 } 51 52 #endregion 53 }
5.2调用过程
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 Console.WriteLine("程序开始"); 6 7 var tmp = Singleton.ReadonlySingleton2.Instance; 8 9 Console.ReadKey(); 10 } 11 }
5.3运行效果
5.4说明
如此写便可避免类一加载就实例化对象。当然还可以有其他写法。
normalX、staticX、normalY、staticY和Instance的执行顺序为:外部调用》staticX 》Instance》normalX》normalY》构造函数》staticY。
可以看出,单例类字段都是在外部第一次调用之后才初始化执行的,这种写法实现了延迟加载。
6.静态初始化单例类示例3(静态构造函数)
6.1代码
1 public sealed class ReadonlySingleton3 2 { 3 #region 字段 4 5 public int normalX = SetValue(staticX); 6 7 public static int staticX = 1; 8 9 #endregion 10 11 #region 单例 12 /// <summary> 13 /// 静态初始化单例,只有通过这种写法并且调用这个字段方能是饿汉式单例 14 /// </summary> 15 public static readonly ReadonlySingleton3 Instance = new ReadonlySingleton3(); 16 17 /// <summary> 18 /// 定义一个静态构造函数,使得静态字段在调用的时候才被初始化 19 /// </summary> 20 static ReadonlySingleton3() 21 { 22 Console.WriteLine("ReadonlySingleton3____Static"); 23 } 24 /// <summary> 25 /// 私有化构造函数,使得类不可通过new来创建实例 26 /// </summary> 27 private ReadonlySingleton3() 28 { 29 Console.WriteLine($"normalX={normalX}"); 30 Console.WriteLine($"staticX={staticX}"); 31 Console.WriteLine($"normalY={normalY}"); 32 Console.WriteLine($"staticY={staticY}"); 33 } 34 35 #endregion 36 37 #region 其他 38 39 public int normalY = SetValue(staticY); 40 41 public static int staticY = 1; 42 43 public void ShowLog() 44 { 45 Console.WriteLine($"{Common.TimeHelper.PrintMillisecond()}:ReadonlySingleton3 ShowLog"); 46 } 47 48 private static int SetValue(int val) 49 { 50 return val; 51 } 52 53 #endregion 54 }
6.2调用过程
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 Console.WriteLine("程序开始"); 6 7 var tmp = Singleton.ReadonlySingleton3.Instance; 8 9 Console.ReadKey(); 10 } 11 }
6.3运行效果
6.4说明
这里定义了一个静态构造函数,使得同为静态类型的单例类实例Instance在第一次调用时才进行初始化,也就达到了不提前占用系统资源的目的。不过在初始化单例类时,会先调用私有构造函数,然后再调用静态构造函数。
normalX、staticX、normalY、staticY和Instance的执行顺序为:外部调用》staticX 》Instance》normalX》normalY》私有构造函数》staticY》静态构造函数。
可以看出,这样写也能达到延迟加载的目的。
7.静态初始化单例类示例4(内部类)
7.1代码
1 public sealed class ReadonlySingleton4 2 { 3 #region 字段 4 5 public int normalX = SetValue(staticX); 6 7 public static int staticX = 1; 8 9 #endregion 10 11 #region 单例 12 private class InnerInstance 13 { 14 internal static ReadonlySingleton4 _instance = new ReadonlySingleton4(); 15 } 16 17 private ReadonlySingleton4() 18 { 19 Console.WriteLine($"normalX={normalX}"); 20 Console.WriteLine($"staticX={staticX}"); 21 Console.WriteLine($"normalY={normalY}"); 22 Console.WriteLine($"staticY={staticY}"); 23 } 24 25 public static ReadonlySingleton4 Instance 26 { 27 get { return InnerInstance._instance; } 28 } 29 30 public static ReadonlySingleton4 GetInstance() 31 { 32 return InnerInstance._instance; 33 } 34 #endregion 35 36 #region 其他 37 38 public int normalY = SetValue(staticY); 39 40 public static int staticY = 1; 41 42 public void ShowLog() 43 { 44 Console.WriteLine($"{Common.TimeHelper.PrintMillisecond()}:ReadonlySingleton4 ShowLog"); 45 } 46 47 private static int SetValue(int val) 48 { 49 return val; 50 } 51 52 #endregion 53 }
7.2调用过程
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 Console.WriteLine("程序开始"); 6 7 var tmp = Singleton.ReadonlySingleton4.Instance; 8 9 Console.ReadKey(); 10 } 11 }
7.3运行效果
7.4说明
normalX、staticX、normalY、staticY和Instance的执行顺序为:外部调用》staticX 》staticY》内部类_instance》normalX》normalY》私有构造函数》内部类静态构造函数(可以不用)。
内部类要不要构造函数和静态构造函数都不影响,都能够达到延迟加载的目的。
8.懒加载Lazy
8.1代码
1 /// <summary> 2 /// 使用Lazy创建单例类示例 3 /// </summary> 4 public sealed class LazySingleton 5 { 6 #region 字段 7 8 public int normalX = SetValue(staticX); 9 10 public static int staticX = 1; 11 12 #endregion 13 14 #region 单例 15 private static readonly Lazy<LazySingleton> _instance = new Lazy<LazySingleton>(() => new LazySingleton()); 16 17 private LazySingleton() 18 { 19 Console.WriteLine($"normalX={normalX}"); 20 Console.WriteLine($"staticX={staticX}"); 21 Console.WriteLine($"normalY={normalY}"); 22 Console.WriteLine($"staticY={staticY}"); 23 } 24 25 //写法1:通过属性获取实例 26 public static LazySingleton Instance 27 { 28 get 29 { 30 return _instance.Value; 31 } 32 } 33 34 //写法2:通过方法获取实例 35 public static LazySingleton GetInstance() 36 { 37 return _instance.Value; 38 } 39 #endregion 40 41 #region 其他 42 43 public int normalY = SetValue(staticY); 44 45 public static int staticY = 1; 46 47 private static int SetValue(int val) 48 { 49 return val; 50 } 51 52 #endregion 53 }
8.2调用过程
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 Console.WriteLine("程序开始"); 6 7 var tmp = Singleton.LazySingleton.Instance; 8 9 Console.ReadKey(); 10 } 11 }
8.3运行效果
8.4说明
normalX、staticX、normalY、staticY和Instance的执行顺序为:外部调用》staticX 》单例对象_instance》staticY》normalX》normalY》私有构造函数。
9.总结
单例模式实现方式很多种,上面只是常用的几种。我认为最适合使用的非"单例私有化"的写法,既避免了多线程安全问题,又能延迟加载,避免提前占用系统资源。
学习了单例模式,接下来继续工厂模式的学习。