前言
单例也是被嚼烂了的设计模式之一,但是这一模式在实际中确实使用非常广泛,今天,使用多个版本的单例模式实现,来讲一下实现单例需要注意的一些地方
版本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(静态初始化,延迟实例化),其实都是可以使用的,具体选择哪个没有标准的,根据实际应用场景来选择吧