单例模式在高并发下不是自动线程安全的;手动实现需用Lazy、静态构造函数或双重检查锁,DI容器注册的Singleton仅在同一个IServiceProvider中全局唯一,且需避免状态竞争与生命周期混用。
不是自动线程安全的。C# 中手动实现的 Singleton 类,如果没做同步控制(比如没用 lock、Lazy 或双重检查锁),首次实例化时可能创建多个实例——尤其在多线程同时调用 Instance 属性时。
常见错误是这样写:
public class MySingleton
{
private static MySingleton _instance;
public static MySingleton Instance => _instance ??= new MySingleton();
}上面的 ??= 在高并发下不是原子操作,_instance 可能被多次赋值。正确做法是用 Lazy:
public class MySingleton
{
private static readonly Lazy _lazy = new Lazy(() => new MySingleton());
public static MySingleton Instance => _lazy.Value;
} Lazy 默认启用线程安全模式(LazyThreadSafetyMode.ExecutionAndPublication)volatile 或内存屏障是的,但前提是:你用的是同一个 IServiceProvider 实例(即同一个 DI 容器根容器)。ASP.NET Core 默认的 WebHostBuilder / HostBuilder 创建的是单根容器,所有请求共享同一组 singleton 实例。
容易踩的坑:
services.BuildServiceProvider() → 每次都新建一个容器,导致 singleton 变成“伪单例”Scoped 或 Transient 服务中持有对 singleton 的引用没问题,但反过来——singleton 里依赖 Scoped 服务(如 DbContext)会引发异常或隐式捕获 scope
SingleInstance() / Singleton() 行为与 Microsoft.Extensions.DependencyInjection 一致DI 容器只保证“实例单一”,不保证“线程安全”。如果你的 singleton 类里有可变字段(private int _counter)、缓存字典(ConcurrentDictionary 除外)、或未加锁的集合操作,就会出现数据竞争。
典型场景:
Dictionary 做运行时缓存 → 高并发读写直接抛 InvalidOperationException
HttpClient 是安全的(它本就是为复用设计),但缓存 HttpClientHandler 并手动设置 Credentials 等属性可能引发副作用async void 或未 await 的 Task → 可能导致 singleton 状态错乱或资源泄漏建议:
lock、SemaphoreSlim)或改用线程安全集合(ConcurrentDictionary、ConcurrentQueue)Scoped)混合使用会导致生命周期失控。例如:你在 Startup.ConfigureServices 注册了 services.AddSingleton,又在某个类里写了 MyService.Instance 手动单例,两个实例各自维护状态,行为完全割裂。
更隐蔽的问题:
IServiceProvider 获取),但该 provider 是从 scoped service 拿的 → 生命周期越界结论:在 ASP.NET Core 项目中,应统一走 DI 容器管理生命周期。手写单例仅限极少数场景(如配置解析器、日志门面封装),且不得参与依赖图。
最常被忽略的一点:singleton 服务的构造函数不能耗时或阻塞(比如连数据库、读大文件),否则会拖慢整个应用启动,甚至触发 Kestrel 启动超时。