今天领导突然问到我,我们之前代码里为什么单例要写if(t == null){ lock(_lock){ if(t == null){ t = new T(); } } }这样的双重if还加锁的写法,于是了解了一下并整理了这边文章。
之前的写法:
public class T
{
private readonly static object _lock = new object();
private static T _instance;
public static T Instance
{
get
{
if(_instance == null)
{
lock(_lock)
{
if(_instance == null)
{
_instance = new T();
}
}
}
return _instance;
}
}
}
逻辑:先判断instance是否被创建,若未被创建则加锁,由于多线程的原因,加锁过后还需再判断instance是否被创建,若此时还未被创建则创建实例,操作完成后释放锁。
为什么要双重if:由于多线程访问,有可能一个线程进入锁的时候,实例已经被其他线程所创建,再加一层判断为了防止其他线程创建过后还有创建实例的操作。
存在问题:
编写繁琐:该写法必须要在每个需要实现单例的对象写这段逻辑;
可读性低:双重 if 的判断和加锁的逻辑不太容易理解,且在不同地方用到是还需注意 if 和 lock 中的逻辑;
效率低:每次获取该实例的时候需要加锁和释放锁,每次加锁和释放锁的操作可能会影响代码运行效率。
优化方案
我主要了解了一种利用微软提供的 Lazy<T>实现懒加载的方法。
public class T
{
private static readonly Lazy<T> lazyInstance = new Lazy<T>(() => new T());
public static T Instance => lazyInstance.Value;
private T()
{
// 构造函数是私有的,以防止直接实例化
}
}
Lazy<T>创建一个延迟初始化的单例实例,lazyInstance是一个静态只读字段,它包含了对Lazy<T>实例的引用。公开Instance属性,提供一个对单例实例的全局访问点,当这个属性第一次被访问时,Lazy<T>会负责初始化单例对象。
优点
简化代码:使用 Lazy<T> 可以避免手动实现双重检查锁定模式,从而简化代码。Lazy<T> 内部已经处理了线程同步的复杂性,开发者只需简单地声明一个 Lazy<T> 实例即可。
线程安全性:Lazy<T> 保证在多线程环境中,即使有多个线程同时尝试访问 Value 属性,也只会创建一个单例实例。Lazy<T> 内部使用高效且安全的同步机制来确保这一点。
性能优化:Lazy<T> 支持延迟初始化,这意味着单例实例只会在第一次访问 Value 属性时被创建。这种方式避免了在程序启动时就创建不必要的对象,从而提高了程序的启动性能。
减少锁竞争:由于 Lazy<T> 的实现细节是封装在 .NET 框架内部的,它可能使用了比手写的同步代码更高效的锁竞争策略。这有助于减少锁竞争,特别是在高并发场景下。
异常安全性:Lazy<T> 在初始化值时能够很好地处理异常。如果在实例化过程中抛出异常,Lazy<T> 会捕获异常并将它存储起来。后续的访问会重新抛出这个异常,而不是创建一个新的实例。
可维护性和可读性:使用 Lazy<T> 实现的单例模式代码更加简洁,易于阅读和维护。它清楚地表达了单例的延迟初始化意图,而不需要深入理解同步机制的细节。
跨平台兼容性:Lazy<T> 是 .NET 标准库的一部分,这意味着使用 Lazy<T> 实现的单例模式可以在不同的 .NET 运行时环境中工作,包括 .NET Framework、.NET Core 和 Xamarin 等。
除此之外还有几种单例的写法:
静态内部类
public class T
{
private T()
{
}
public static T getInstance()
{
return Inner.single;
}
private static class Inner
{
internal static T single = new T();
}
}
CAS
public class T
{
private static T single;
private T()
{
}
public static T getInstance()
{
if (single != null) return single;
var v = new T();
Interlocked.CompareExchange(ref single, v, null);
return single;
}
}