在C#开发中,单例模式用于确保某个类在应用程序域内只有一个实例,并提供全局访问点。看似简单的模式,在多线程场景里却暗藏不少坑。如果实现不当,不仅无法保证唯一性,还可能因为锁竞争拖慢系统性能。

一、为什么需要线程安全的单例
单例模式的核心目标是控制实例数量。在单线程程序中,先判断静态字段是否为空,为空就new一个,这种写法不会出问题。但现代应用几乎都是多线程的,比如Web服务器每个请求由一个线程处理,如果这些线程同时第一次访问单例,都可能看到字段为null,于是各自创建一个对象,最终内存中出现多个实例。
多个实例会导致严重后果。例如一个负责写日志的单例,如果变成多个,日志文件可能被不同对象轮流打开关闭,造成内容错乱;又比如配置管理器,不同线程读到不同配置副本,系统行为将不可预测。因此线程安全不是可选项,而是单例实现的基本要求。
二、基于静态字段的饿汉式写法
最简单且由CLR保障安全的写法是使用静态只读字段。C#在加载类型时,会执行静态字段初始化,这个过程由运行时加锁,确保一个类型只初始化一次。我们将实例直接赋给静态只读变量,构造函数设为私有,外部就无法new,也只能拿到同一个对象。
这种写法属于饿汉式,因为程序启动、类型被访问前实例就已经建好。优点是完全不用自己写锁,代码极简且绝对安全;缺点是如果实例构造很耗时或占用大量内存,而程序全程没用到它,就白做了准备。对于大多数业务单例,这点开销可以忽略。
public sealed class Logger
{
// 静态只读字段,CLR在类型初始化时创建,线程安全
private static readonly Logger instance = new Logger();
// 私有构造函数,阻止外部new
private Logger()
{
// 初始化日志路径等
}
public static Logger Instance
{
get { return instance; }
}
public void Write(string msg)
{
// 写日志逻辑
}
}
三、懒汉式与双重检查锁
如果希望实例在第一次使用时才创建,可以用懒汉式。最直白的懒汉写法是在Instance属性里加lock,但每次获取都加锁会带来性能损耗。为此引出双重检查锁:先判断是否为null,再加锁,进锁后再判断一次,避免多个线程排队后重复创建。
这里有个关键细节,实例字段要标volatile,否则编译器或CPU可能重排指令,让对象引用先被赋值、构造函数还没执行完,另一个线程就会拿到一个半成品对象。下面代码演示了正确写法,既实现了懒加载,又保证了线程安全和较高性能。
public sealed class ConfigManager
{
private static volatile ConfigManager instance = null;
private static readonly object lockObj = new object();
private ConfigManager()
{
// 从文件加载配置
}
public static ConfigManager Instance
{
get
{
if (instance == null)
{
lock (lockObj)
{
if (instance == null)
{
instance = new ConfigManager();
}
}
}
return instance;
}
}
}
四、使用Lazy<T>简化懒加载
从.NET 4.0开始,框架提供了Lazy<T>类型,它内部已经实现了线程安全的延迟初始化。我们只需要把工厂方法传进去,运行时会在第一次访问Value时创建对象,且默认采用执行模式保证只建一次。这比手写双重检查锁更不容易出错。
使用Lazy<T>的代码可读性很好,也明确表达了延迟加载意图。如果你的项目基于较新版本的.NET,推荐优先考虑这种方式,把并发细节交给基类库处理,自己专注业务逻辑。
public sealed class CacheService
{
private static readonly Lazy<CacheService> lazy =
new Lazy<CacheService>(() => new CacheService());
public static CacheService Instance
{
get { return lazy.Value; }
}
private CacheService()
{
// 建立内存缓存
}
}
五、常见陷阱与选用建议
第一个陷阱是忘了把构造函数私有化,结果别人随手new就破坏了单例。第二个陷阱是在单例里存了非线程安全的成员,比如普通Dictionary,即便实例唯一,多个线程改字典仍会崩。此时应改用ConcurrentDictionary或加锁访问。
选型上,若实例轻量且必用,静态只读字段最省心;若要懒加载又不想管锁,用Lazy<T>;只有在维护老代码或特殊框架限制时才手写双重检查锁。理解这些写法背后的CLR行为和内存模型,才能写出真正健壮的C#单例。
C#singletonthread_safe修改时间:2026-08-01 02:57:35