单例模式在高并发下不是自动线程安全的;手动实现需用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或内存屏障 - 静态构造函数也可保证线程安全,但无法延迟初始化
DI 容器注册为 Singleton 时,实例真的全局唯一吗?
是的,但前提是:你用的是同一个 IServiceProvider 实例(即同一个 DI 容器根容器)。ASP.NET Core 默认的 WebHostBuilder / HostBuilder 创建的是单根容器,所有请求共享同一组 singleton 实例。
容易踩的坑:
- 在中间件或控制器里手动调用
services.BuildServiceProvider()→ 每次都新建一个容器,导致 singleton 变成“伪单例” - 在
Scoped或Transient服务中持有对 singleton 的引用没问题,但反过来——singleton 里依赖Scoped服务(如DbContext)会引发异常或隐式捕获 scope - 使用第三方容器(如 Autofac、DryIoc)时,确认其
SingleInstance()/Singleton()行为与 Microsoft.Extensions.DependencyInjection 一致
高并发下 singleton 服务里的状态管理风险
DI 容器只保证“实例单一”,不保证“线程安全”。如果你的 singleton 类里有可变字段(private int _counter)、缓存字典(ConcurrentDictionary 除外)、或未加锁的集合操作,就会出现数据竞争。
典型场景:
- 用
Dictionary做运行时缓存 → 高并发读写直接抛InvalidOperationException - 在 singleton 中缓存
HttpClient是安全的(它本就是为复用设计),但缓存HttpClientHandler并手动设置Credentials等属性可能引发副作用 - 异步方法中用
async void或未 await 的Task→ 可能导致 singleton 状态错乱或资源泄漏
建议:
- 优先用不可变对象、纯函数逻辑
- 状态变更必须加锁(
lock、SemaphoreSlim)或改用线程安全集合(ConcurrentDictionary、ConcurrentQueue) - 避免在 singleton 中存储 request/session 级别数据(该用
Scoped)
DI 容器和手写单例混用会出什么问题?
混合使用会导致生命周期失控。例如:你在 Startup.ConfigureServices 注册了 services.AddSingleton,又在某个类里写了 MyService.Instance 手动单例,两个实例各自维护状态,行为完全割裂。
更隐蔽的问题:
- 手写单例里依赖了 DI 容器注入的服务(比如通过
IServiceProvider获取),但该 provider 是从 scoped service 拿的 → 生命周期越界 - 单元测试时,手写单例无法被替换或重置,破坏可测性
- 某些 DI 容器(如 Scrutor)支持装饰器、条件注册,手写单例绕过了这些机制
结论:在 ASP.NET Core 项目中,应统一走 DI 容器管理生命周期。手写单例仅限极少数场景(如配置解析器、日志门面封装),且不得参与依赖图。
最常被忽略的一点:singleton 服务的构造函数不能耗时或阻塞(比如连数据库、读大文件),否则会拖慢整个应用启动,甚至触发 Kestrel 启动超时。










