单例模式是设计模式中最基础也最常用的模式之一,在C#开发中,实现单例模式需要兼顾线程安全、实例唯一性和访问便捷性等多个维度,不同的实现方式对应不同的使用场景。

C#单例模式的核心要求
一个标准的单例模式实现需要满足三个核心条件:
- 类的构造方法必须是私有的,防止外部通过new关键字创建实例
- 类内部需要维护一个自身的静态实例
- 提供一个公共的静态方法或者属性,用于外部获取唯一的实例
常见的C#单例实现方法
1. 饿汉式单例
饿汉式单例是在类加载时就直接创建实例,实现简单且线程安全,但是会提前占用内存。
public class HungrySingleton
{
// 类加载时就初始化实例
private static readonly HungrySingleton instance = new HungrySingleton();
// 私有构造方法,防止外部实例化
private HungrySingleton() { }
// 公共访问点
public static HungrySingleton Instance
{
get { return instance; }
}
// 示例方法
public void DoSomething()
{
Console.WriteLine("饿汉式单例执行方法");
}
}
2. 简单懒汉式单例(非线程安全)
懒汉式单例是在第一次调用获取实例的方法时才创建实例,但是这种基础实现方式在多线程环境下会出现多个实例的问题。
public class LazySingletonUnsafe
{
private static LazySingletonUnsafe instance = null;
private LazySingletonUnsafe() { }
public static LazySingletonUnsafe Instance
{
get
{
// 多线程同时进入这里时,可能会创建多个实例
if (instance == null)
{
instance = new LazySingletonUnsafe();
}
return instance;
}
}
}
3. 双重检查锁定单例
双重检查锁定是懒汉式单例的线程安全优化版本,既保证了线程安全,又减少了锁的开销,适合多线程场景。
public class DoubleCheckSingleton
{
// 使用volatile关键字保证多线程下变量的可见性
private static volatile DoubleCheckSingleton instance = null;
// 线程锁对象
private static readonly object lockObj = new object();
private DoubleCheckSingleton() { }
public static DoubleCheckSingleton Instance
{
get
{
// 第一次检查,避免不必要的锁开销
if (instance == null)
{
lock (lockObj)
{
// 第二次检查,确保只有一个实例被创建
if (instance == null)
{
instance = new DoubleCheckSingleton();
}
}
}
return instance;
}
}
}
4. 静态内部类单例
利用C#静态内部类的特性,只有在调用内部类的静态成员时才会加载内部类,从而实现懒加载和线程安全,代码更简洁。
public class InnerClassSingleton
{
private InnerClassSingleton() { }
// 静态内部类,只有被调用时才会加载
private static class SingletonHolder
{
internal static readonly InnerClassSingleton instance = new InnerClassSingleton();
}
public static InnerClassSingleton Instance
{
get { return SingletonHolder.instance; }
}
}
5. 泛型单例基类
如果项目中多个类都需要实现单例模式,可以封装一个泛型单例基类,减少重复代码。
public abstract class GenericSingleton<T> where T : class
{
private static T instance = null;
private static readonly object lockObj = new object();
protected GenericSingleton() { }
public static T Instance
{
get
{
if (instance == null)
{
lock (lockObj)
{
if (instance == null)
{
// 通过反射创建实例,要求T有私有构造方法
var constructor = typeof(T).GetConstructor(
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance,
null, Type.EmptyTypes, null);
if (constructor == null)
{
throw new InvalidOperationException("类型必须包含私有无参构造方法");
}
instance = constructor.Invoke(null) as T;
}
}
}
return instance;
}
}
}
// 使用示例
public class UserService : GenericSingleton<UserService>
{
private UserService() { }
public void AddUser()
{
Console.WriteLine("添加用户");
}
}
不同实现方式的对比
我们可以通过下表对比不同单例实现方式的特点,方便选择:
| 实现方式 | 线程安全 | 懒加载 | 实现复杂度 | 适用场景 |
|---|---|---|---|---|
| 饿汉式单例 | 是 | 否 | 低 | 实例占用资源小,不需要懒加载的场景 |
| 简单懒汉式 | 否 | 是 | 低 | 单线程环境,不需要线程安全的场景 |
| 双重检查锁定 | 是 | 是 | 中 | 多线程环境,需要懒加载的场景 |
| 静态内部类 | 是 | 是 | 中 | 多线程环境,需要懒加载且代码简洁的场景 |
| 泛型单例基类 | 是 | 是 | 高 | 多个类需要实现单例,减少重复代码的场景 |
单例模式的使用注意事项
虽然单例模式使用广泛,但是也存在一些需要注意的问题:
- 单例类的构造方法是私有的,所以无法被继承,也不方便进行单元测试,因为单例的状态是全局共享的
- 如果单例实例持有大量资源,可能会导致内存占用过高,尤其是在应用程序生命周期很长的情况下
- 不要过度使用单例模式,很多场景下可以通过依赖注入的方式管理对象生命周期,比单例模式更灵活
在实际开发中,推荐优先使用静态内部类或者双重检查锁定的方式实现单例,这两种方式兼顾了线程安全和懒加载的需求,适配大多数使用场景。如果是.NET 4.0及以上版本,还可以使用Lazy<T>类型来实现单例,进一步简化代码。