前言
单例也是被嚼烂了的设计模式之一,但是这一模式在实际中确实使用非常广泛,今天,使用多个版本的单例模式实现,来讲一下实现单例需要注意的一些地方
版本1
使用静态字段,使用单例方法(属性)进行获取,第一次访问时进行初始化,以后直接返回
public sealed class Singleton { // 私有构造函数,禁止外部访问 private Singleton() { } // 静态实例字段 private static Singleton _instance; // 静态实例方法 public static Singleton Instance { get { if (_instance == null) _instance = new Singleton(); return _instance; } } }
版本2
OK,单例方法的设计需要考虑多线程调用,所以线程同步是必须考虑的,否则可能就不是真的“单例”了
public sealed class Singleton { // 私有构造函数,禁止外部访问 private Singleton() { } // 静态实例字段 private static volatile Singleton _instance; private static readonly object syncRoot = new object(); // 静态实例方法 public static Singleton Instance { get { lock (syncRoot) { if (_instance == null) _instance = new Singleton(); return _instance; } } } }
其实在实际使用中,这个版本已经OK了,但是线程同步还可以做一点小小的优化,于是
版本3
public sealed class Singleton { // 私有构造函数,禁止外部访问 private Singleton() { } // 静态实例字段 private static volatile Singleton _instance; private static readonly object syncRoot = new object(); // 静态实例方法 public static Singleton Instance { get { if (_instance == null) { lock (syncRoot) { if (_instance == null) _instance = new Singleton(); } } return _instance; } } }
OK,现在这个版本已经完美了,可以使用了,而且线程同步进行了优化,不用每一次调用都进行lock操作,但是等等,很多人还有另一种风格,就是在静态字段中提供内联初始化或者默认初始化,然后让属性或单例方法直接返回该静态字段。因为使用静态字段的初始化语法,其实可以保证线程安全(CLR是这么处理的),所以不用我们自己去编写线程同步代码
版本4
public sealed class Singleton { // 私有构造函数,禁止外部访问 private Singleton() { Console.WriteLine("Singleton Constructed"); } // 静态实例字段 private static readonly Singleton _instance = new Singleton(); // 静态实例方法 public static Singleton Instance { get { return _instance; } } }
OK,现在不用在获取实例的方法(属性)中写构造表达式了,更不用自己写线程同步的代码了,但是现在有一个问题,那就是字段的内联初始化的初始化时间是提前的,而且是不确定的,因为没有提供默认静态构造函数的话,静态字段的内联初始化会生成beforeInit标记,字段的初始化会在使用类型以前随机寻找一个时间来调用,这可以适度优化
版本5
public sealed class Singleton { // 私有构造函数,禁止外部访问 private Singleton() { Console.WriteLine("Singleton Constructed"); } // 默认静态构造函数 static Singleton() { } // 静态实例字段 private static readonly Singleton _instance = new Singleton(); // 静态实例方法 public static Singleton Instance { get { return _instance; } } }
看到区别了么?现在只是加了一个静态构造函数,为了防止内联过早的进行调用,现在不会生出fieldBeforeInit信息,所以会到调用之前才进行初始化。但是关于调用的时机,是不是依然可以优化,优化到真正调用时才进行初始化呢?好了,请看下一个版本
版本6
public class Singleton { // 私有构造函数,禁止外部访问 private Singleton() { Console.WriteLine("Singleton Constructed"); } public static Singleton Instance { get { return NestedObject.NestedInstance; } } private class NestedObject { static NestedObject() { } public static readonly Singleton NestedInstance = new Singleton(); } }
这个版本使用内嵌的私有类作为一个容器来维持单例对象,使用静态初始化的方式并且支持延迟初始化,非常聪明。那么是否到此为止呢?其实对于静态初始化的延迟执行,.NET提供了非常方便的Lazy类,我们何不拿来一用,这样我们就不用自己编写内部类了
版本7
public class Singleton { private static Lazy<Singleton> _instance = new Lazy<Singleton>(() => new Singleton()); // 私有构造函数,禁止外部访问 private Singleton() { Console.WriteLine("Singleton Constructed"); } public static Singleton Instance { get { return _instance.Value; } } }
总结
大家对单例模式的说法一向都是简约不简单,所以此模式虽然已被嚼烂,但它依然是作为程序员的必修课之一,这里面考察的主要是
- 类的修饰符,一般建议使用 sealed,密封类,不允许继承
- 类的构造函数,一般建议private,不允许外部访问
- 类的执行时机,根据实际需求判断,是否饿汉式初始化或者惰性初始化
- 类的初始化线程安全的考虑,必须考虑多线程访问时,构造仅执行一次
实践中,可用的方式包括,版本3(单例方法初始化,加双检查锁定,延迟初始化)、版本4、5(静态初始化,非延迟初始化)、版本6、7(静态初始化,延迟实例化),其实都是可以使用的,具体选择哪个没有标准的,根据实际应用场景来选择吧