标签:线程安全、延迟初始化、按需初始化、提升启动性能
项目地址:NitasDemo/12Lazy/LazyDemo at main · Nita121388/NitasDemo
目录
Lazy
官方文档:Lazy<T> 类 (System) | Microsoft Learn
源码:Lazy.cs
1. 概念
Lazy<T>
是 .NET 4.0 引入的泛型类,实现延迟初始化。
它确保对象仅在首次访问时被创建,从而避免不必要的资源消耗并提升启动性能。
该类封装了延迟初始化的逻辑,并提供了线程安全的初始化机制。
特性
特性 说明 延迟初始化 对象在第一次访问 `.Value` 时才被实例化,提升启动性能。 节省资源 如果对象从未被使用,则不会创建,节省内存和 CPU。 线程安全 默认支持多线程环境下的安全初始化(可设置线程安全模式) 可复用 一旦初始化完成,后续访问 `.Value` 将直接返回已创建的对象,不会重复创建。 支持复杂逻辑 可通过委托传入自定义初始化逻辑。 优点:
减少启动时间,按需加载资源。
缺点:
- 首次访问可能有延迟。
- 线程安全模式存在锁竞争和异常放大,滥用会浪费性能
- 过度使用会导致代码可读性下降。
最佳实践:
仅对真正需要延迟初始化的对象使用
Lazy<T>
。
2. 基本用法
用法1:默认构造函数(要求无参构造)
// 简单创建(非线程安全) Lazy<ExpensiveObject> lazySimple = new Lazy<ExpensiveObject>(); ExpensiveObject obj = lazySimple.Value; // 首次访问时初始化
用法2:使用委托自定义初始化逻辑
Lazy<MyClass> lazy = new Lazy<MyClass>(() => new MyClass("自定义构造"));
3. 异常处理
初始化异常被缓存:
首次初始化失败后,异常会在每次调用时重新抛出。
解决方案:
使用
Lazy<T>
的构造函数重载捕获异常并重试。
Lazy<ExpensiveObject> lazyWithRetry = new Lazy<ExpensiveObject>(() =>
{
try { return new ExpensiveObject(); }
catch { /* 重试逻辑 */ }
});
4. 线程安全模式
通过 LazyThreadSafetyMode
指定初始化行为:
构造函数
public Lazy(Func<T> valueFactory, LazyThreadSafetyMode mode);
模式枚举值 含义说明 适用场景 `ExecutionAndPublication` 线程安全,确保只有一个线程执行初始化逻辑。 多线程环境(默认) `PublicationOnly` 多线程下允许多个线程同时初始化,但只保留第一个完成的实例。 避免加锁阻塞、初始化成本高、重复初始化无影响,允许短暂浪费资源时 `None` 非线程安全 单线程环境(高性能) ExecutionAndPublication
模式部分源码private void ExecutionAndPublication(LazyHelper executionAndPublication, bool useDefaultConstructor) { lock (executionAndPublication) { // it's possible for multiple calls to have piled up behind the lock, so we need to check // to see if the ExecutionAndPublication object is still the current implementation. if (ReferenceEquals(_state, executionAndPublication)) { if (useDefaultConstructor) { ViaConstructor(); } else { ViaFactory(LazyThreadSafetyMode.ExecutionAndPublication); } } } }
PublicationOnly
模式部分源码private void PublicationOnly(LazyHelper publicationOnly, T possibleValue) { LazyHelper? previous = Interlocked.CompareExchange(ref _state, LazyHelper.PublicationOnlyWaitForOtherThreadToPublish, publicationOnly); if (previous == publicationOnly) { _factory = null; _value = possibleValue; _state = null; // volatile write, must occur after setting _value } }
// 线程安全模式(默认:确保仅初始化一次)
Lazy<ExpensiveObject> safeLazy = new Lazy<ExpensiveObject>(
() => new ExpensiveObject(),
LazyThreadSafetyMode.ExecutionAndPublication
);
//避免加锁阻塞、初始化成本高、允许短暂浪费资源时,使用 PublicationOnly
Lazy<ExpensiveCache> cache = new Lazy<ExpensiveCache>(
() => new ExpensiveObject(),
LazyThreadSafetyMode.PublicationOnly);
// 非线程安全模式(高性能单线程场景)
Lazy<ExpensiveObject> unsafeLazy = new Lazy<ExpensiveObject>(
() => new ExpensiveObject(),
LazyThreadSafetyMode.None
);
5. 示例
1. 线程安全模式 (ExecutionAndPublication
)
场景:全局配置加载
using System;
using System.Threading;
using System.Threading.Tasks;
#region 模拟配置类 Configuration
public class Configuration
{
public string Environment { get; set; }
public int MaxConnections { get; set; }
public DateTime LoadTime { get; set; }
public override string ToString() =>
$"[{Environment}] MaxConnections={MaxConnections}, LoadTime={LoadTime:HH:mm:ss.fff}";
}
#endregion 模拟配置类
#region 配置服务使用线程安全的Lazy初始化
public class AppConfigService
{
private static int _loadCounter = 0; // 用于跟踪实际加载次数
private static readonly Lazy<Configuration> _config =
new Lazy<Configuration>(() =>
{
Interlocked.Increment(ref _loadCounter);//记录实际初始化次数
Console.WriteLine($">>> [线程 {Thread.CurrentThread.ManagedThreadId}] 开始加载配置...");
Thread.Sleep(2000); // 模拟数据库/IO延迟
return new Configuration {
Environment = "Production",
MaxConnections = 100,
LoadTime = DateTime.Now
};
}, LazyThreadSafetyMode.ExecutionAndPublication);
public static Configuration Config => _config.Value;
public static int LoadCount => _loadCounter;
}
#endregion
#region Usage
class Program
{
static void Main(string[] args)
{
Console.WriteLine("=== 配置加载测试(线程安全模式)===");
Console.WriteLine($"主线程ID: {Thread.CurrentThread.ManagedThreadId}\n");
// 创建10个并发请求线程
Parallel.For(0, 10, i => {
Thread.Sleep(new Random().Next(50)); // 随机延迟增加并发冲突概率
Console.WriteLine($"[线程 {Thread.CurrentThread.ManagedThreadId}] 请求配置...");
var config = AppConfigService.Config;
Console.WriteLine($"[线程 {Thread.CurrentThread.ManagedThreadId}] 获取配置: {config}");
});
Console.WriteLine($"\n实际加载次数: {AppConfigService.LoadCount}");
Console.WriteLine("测试完成。按任意键退出...");
Console.ReadKey();
}
}
#endregion
输出:
=== 配置加载测试(线程安全模式)===
主线程ID: 1
[线程 12] 请求配置...
>>> [线程 12] 开始加载配置...
[线程 6] 请求配置...
[线程 7] 请求配置...
[线程 8] 请求配置...
[线程 9] 请求配置...
[线程 4] 请求配置...
[线程 10] 请求配置...
[线程 1] 请求配置...
[线程 11] 请求配置...
[线程 13] 请求配置...
[线程 1] 获取配置: [Production] MaxConnections=100, LoadTime=16:09:44.947
[线程 9] 获取配置: [Production] MaxConnections=100, LoadTime=16:09:44.947
[线程 11] 获取配置: [Production] MaxConnections=100, LoadTime=16:09:44.947
[线程 6] 获取配置: [Production] MaxConnections=100, LoadTime=16:09:44.947
[线程 13] 获取配置: [Production] MaxConnections=100, LoadTime=16:09:44.947
[线程 7] 获取配置: [Production] MaxConnections=100, LoadTime=16:09:44.947
[线程 8] 获取配置: [Production] MaxConnections=100, LoadTime=16:09:44.947
[线程 12] 获取配置: [Production] MaxConnections=100, LoadTime=16:09:44.947
[线程 4] 获取配置: [Production] MaxConnections=100, LoadTime=16:09:44.947
[线程 10] 获取配置: [Production] MaxConnections=100, LoadTime=16:09:44.947
实际加载次数: 1
测试完成。按任意键退出...
为何适用:
- 配置加载成本高(数据库查询)
- 需确保所有线程获取同一实例
- 需避免重复初始化导致资源浪费
2. 发布模式 (PublicationOnly
)
场景:轻量级日志记录器
using System;
using System.Collections.Concurrent;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace PublicationOnlyLoggerDemo
{
// 日志接口
public interface ILogger
{
void Log(string message);
}
// 线程安全的文件日志记录器
public class FileLogger : ILogger
{
private readonly string _filePath;
// 静态锁确保多实例写入时的线程安全
private static readonly object _fileLock = new object();
// 记录已创建实例数量(用于演示)
public static int InstanceCount = 0;
// 记录实际写入次数(用于演示)
public static readonly ConcurrentBag<string> AllLogs = new ConcurrentBag<string>();
public FileLogger(string filePath)
{
if (!File.Exists(filePath))
{
File.Create(filePath).Close();
}
_filePath = filePath;
Interlocked.Increment(ref InstanceCount);
lock (_fileLock)
{
// 初始化日志文件
File.WriteAllText(filePath, $"Log initialized at {DateTime.Now:HH:mm:ss.fff}\n");
}
}
public void Log(string message)
{
lock (_fileLock)
{
File.AppendAllText(_filePath, $"{DateTime.Now:HH:mm:ss.fff} - {message}\n");
}
AllLogs.Add(message);
}
}
// 日志工厂(使用PublicationOnly模式)
public static class LoggerFactory
{
private static readonly Lazy<ILogger> _logger =
new Lazy<ILogger>(
() => new FileLogger("app.log"),
LazyThreadSafetyMode.PublicationOnly
);
public static ILogger GetLogger() => _logger.Value;
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("===开启并行日志记录测试===");
// 并行日志记录测试
Parallel.For(0, 10, i =>
{
var logger = LoggerFactory.GetLogger();
logger.Log($"Task {i} 开启");
Thread.Sleep(50); // 模拟工作负载
logger.Log($"Task {i} 完成");
});
// 显示统计结果
Console.WriteLine("\n测试结果:");
Console.WriteLine($"日志实例创建个数: {FileLogger.InstanceCount}");
Console.WriteLine($"总计写入日志条目:{FileLogger.AllLogs.Count}");
Console.WriteLine($"首次使用的日志实例:{FileLogger.AllLogs.First()}");
Console.WriteLine($"最后使用的日志实例:{FileLogger.AllLogs.Last()}");
Console.WriteLine("\n日志文件内容:");
Console.WriteLine("-----------------");
Console.WriteLine(File.ReadAllText("app.log"));
Console.WriteLine("\n按任意键退出...");
Console.ReadKey();
}
}
}
输出:
测试结果:
日志实例创建个数: 10
总计写入日志条目:20
首次使用的日志实例:Task 3 完成
最后使用的日志实例:Task 5 开启
日志文件内容:
-----------------
Log initialized at 10:13:27.464
10:13:27.464 - Task 3 开启
10:13:27.518 - Task 0 完成
10:13:27.518 - Task 2 完成
10:13:27.518 - Task 4 完成
10:13:27.518 - Task 3 完成
10:13:27.518 - Task 5 完成
10:13:27.519 - Task 7 完成
10:13:27.519 - Task 8 完成
10:13:27.519 - Task 1 完成
10:13:27.519 - Task 6 完成
10:13:27.519 - Task 9 完成
按任意键退出...
"
===开启并行日志记录测试===
测试结果:
日志实例创建个数: 10
总计写入日志条目:20
首次使用的日志实例:Task 3 完成
最后使用的日志实例:Task 5 开启
日志文件内容:
-----------------
Log initialized at 10:13:27.464
10:13:27.464 - Task 3 开启
10:13:27.518 - Task 0 完成
10:13:27.518 - Task 2 完成
10:13:27.518 - Task 4 完成
10:13:27.518 - Task 3 完成
10:13:27.518 - Task 5 完成
10:13:27.519 - Task 7 完成
10:13:27.519 - Task 8 完成
10:13:27.519 - Task 1 完成
10:13:27.519 - Task 6 完成
10:13:27.519 - Task 9 完成
按任意键退出...
为何适用:
- 日志初始化简单(创建文件句柄)
- 可容忍短暂存在多个日志实例
- 避免锁竞争提升性能
3. 结合依赖注入 (DI)
场景:解决循环依赖
遵循了依赖倒置原则,通过引入抽象层(Lazy代理)解耦了服务之间的直接依赖关系,解决循环依赖问题 。
using System;
using Microsoft.Extensions.DependencyInjection;
public class Program
{
static void Main(string[] args)
{
// 设置依赖注入容器
var services = new ServiceCollection();
// 注册服务(注意顺序很重要)
services.AddScoped<PaymentService>();
services.AddScoped<OrderService>();
// 使用Lazy打破循环依赖
services.AddScoped(sp =>
new Lazy<OrderService>(() => sp.GetRequiredService<OrderService>()));
using var serviceProvider = services.BuildServiceProvider();
// 模拟请求范围
using (var scope = serviceProvider.CreateScope())
{
var scopedProvider = scope.ServiceProvider;
Console.WriteLine("解析OrderService...");
var orderService = scopedProvider.GetRequiredService<OrderService>();
Console.WriteLine("\n调用OrderService处理订单:");
orderService.ProcessOrder(100.50m);
}
Console.WriteLine("\n按任意键退出...");
Console.ReadKey();
}
}
public class OrderService
{
private readonly PaymentService _paymentService;
// 正常依赖PaymentService
public OrderService(PaymentService paymentService)
{
Console.WriteLine(">>> OrderService 已创建");
_paymentService = paymentService;
}
public void ProcessOrder(decimal amount)
{
Console.WriteLine($"处理订单: ${amount}");
_paymentService.ProcessPayment(amount);
// 模拟其他操作
Console.WriteLine("订单处理完成!");
}
public Order GetCurrentOrder() => new Order(DateTime.Now, 100.50m);
}
public class PaymentService
{
private readonly Lazy<OrderService> _lazyOrderService;
// 通过Lazy间接依赖OrderService
public PaymentService(Lazy<OrderService> lazyOrderService)
{
Console.WriteLine(">>> PaymentService 已创建");
_lazyOrderService = lazyOrderService;
}
public void ProcessPayment(decimal amount)
{
Console.WriteLine($"处理支付: ${amount}");
// 按需访问OrderService(实际使用时才解析)
Console.WriteLine("\n需要订单信息,访问Lazy.Value...");
var currentOrder = _lazyOrderService.Value.GetCurrentOrder();
Console.WriteLine($"获取到当前订单: {currentOrder}");
}
}
public record Order(DateTime CreatedTime, decimal Amount)
{
public override string ToString() =>
$"[{CreatedTime:HH:mm:ss}] ${Amount}";
}
输出
解析OrderService...
>>> PaymentService 已创建
>>> OrderService 已创建
调用OrderService处理订单:
处理订单: $100.50
处理支付: $100.50
需要订单信息,访问Lazy.Value...
获取到当前订单: [10:44:31] $100.50
按任意键退出...
说明:
遵循了依赖倒置原则,引入抽象层(Lazy代理),解耦了服务之间的直接依赖关系,解决循环依赖问题 。
依赖倒置(DIP,Dependency Inversion Principle)设计
- OrderService → 直接依赖 PaymentService
- PaymentService → 依赖
Lazy<OrderService>
(非直接依赖) - 关键点:将强依赖转换为弱依赖
阶段说明
构造阶段:只需要Lazy代理,不触发实际解析
- PaymentService 只需要一个"承诺"(Lazy代理),不需要实际 OrderService 实例
- 避免初始化死锁
执行阶段:依赖树已建立,安全访问
- 首次访问
.Value
触发实际解析 - 此时 OrderService 已完全初始化
- 后续访问使用缓存实例
- 首次访问
为何适用
- 解决紧耦合服务的循环引用问题
- 延迟初始化高开销服务(如数据库连接)
- 动态插件加载(
Lazy<IPlugin>
按需激活)
使用场景总结
场景 | 技术选型 | 理由 |
---|---|---|
全局配置/缓存 | 线程安全模式 | 多线程共享 + 单次初始化 |
轻量级工具类(如日志) | 发布模式 | 允许冗余初始化 + 避免锁性能损耗 |
UI组件/单线程模块 | 非线程安全模式 | 明确线程上下文 + 零开销 |
DI容器中的复杂服务 | `Lazy<T>`注入 | 打破循环依赖 + 按需加载 |
网络请求/文件加载 | `Lazy<Task<T>>` 或 `AsyncLazy<T>` | 非阻塞初始化 + 结果缓存 |
6. 注意事项与最佳实践
注意点 | 建议做法 |
---|---|
构造函数中不要访问 `.Value` | 会导致死循环或异常 |
避免在委托中抛出异常 | 使用 try-catch 包裹初始化逻辑,或设置异常处理策略 |
使用 `IsValueCreated` 检查状态 | 可用于调试或日志输出 |
7. 总结
优势:
平衡性能与资源,提供线程安全的按需初始化。
注意:
- 优先选择默认线程安全模式。
- 避免在频繁调用的方法中滥用。 (存在锁竞争/异常放大)
- 谨慎处理初始化异常。
LazyInitializer
官方文档:LazyInitializer.EnsureInitialized 方法 (System.Threading) | Microsoft Learn
1. 概念
- 基本信息
- 命名空间:
System.Threading
- 程序集:
System.Threading.dll
- 继承关系:
Object
→LazyInitializer
- 命名空间:
- 特性
特性 说明 零分配开销 避免创建专用延迟初始化实例,比 `Lazy<T>` 更轻量(无额外对象分配) 引用初始化 通过引用传递确保初始化状态一致性 线程安全 内部通过锁或原子操作保证线程安全,支持多线程并发调用 静态方法 直接操作字段,无需创建包装器 异常处理 初始化函数抛出异常时,后续访问会重试(与 `Lazy<T>` 的缓存异常不同) 轻量级替代方案 相比 `Lazy<T>` ,无需额外包装对象,直接操作目标字段。 - 示例
ExpensiveData _data = null; bool _dataInitialized = false; object _dataLock = new object(); // 使用示例 ExpensiveData dataToUse = LazyInitializer.EnsureInitialized( ref _data, ref _dataInitialized, ref _dataLock);
2. 方法重载详解
🔧 方法列表
EnsureInitialized\<T>(T) 在目标引用或值类型尚未初始化的情况下,使用其类型的无参数构造函数初始化目标引用类型。 EnsureInitialized\<T>(T, Boolean, Object) 在目标引用或值类型尚未初始化的情况下,使用其无参数构造函数对其进行初始化。 EnsureInitialized\<T>(T, Boolean, Object, Func\<T>) 在目标引用或值类型尚未初始化的情况下,使用指定函数初始化目标引用或值类型。 EnsureInitialized\<T>(T, Func\<T>) 在目标引用类型尚未初始化的情况下,使用指定函数初始化目标引用类型。 EnsureInitialized\<T>(T, Object, Func\<T>) 在目标引用类型尚未初始化的情况下,使用指定函数初始化目标引用类型。 重载 1:默认构造函数
public static T EnsureInitialized<T> (ref T? target) where T : class;
- 适用场景:类型有无参构造函数
- 线程安全:低竞争环境下安全(可能多次构造但最终保留一个实例,类似与Lazy<T>的
PublicationOnly
模式) - 返回: 已初始化的对象。
- 示例:
private ExpensiveResource _resource; public ExpensiveResource Resource => LazyInitializer.EnsureInitialized(ref _resource);
重载 2:自定义初始化函数
public static T EnsureInitialized<T> (ref T? target, Func<T> valueFactory) where T : class;
- 适用场景:需参数化构造或复杂初始化逻辑
- 线程安全:低竞争环境下安全(可能多次构造但最终保留一个实例)
- 返回: 已初始化的对象。
- 示例:
private DatabaseConnection _db; public DatabaseConnection Db => LazyInitializer.EnsureInitialized(ref _db, () => new DatabaseConnection(_config));
重载 3:完全线程安全控制
public static T EnsureInitialized<T> (ref T target, ref bool initialized, ref object? syncLock, Func<T> valueFactory);
- 适用场景:高并发环境要求严格单次初始化
- 参数说明:
ref T target
:需确保初始化的目标对象。 如果是null
,则将其视为未初始化;否则,将其视为已初始化。ref bool initialized
:跟踪初始化状态。如果initialized
指定为 true,则不会进一步初始化。ref object syncLock
:同步锁对象(可传入null
,方法内部初始化)Func<T> valueFactory
:创建对象的工厂方法,未初始化时调用此方法生成新实例,支持自定义逻辑(如构造函数、依赖注入等)。
- 示例:
private Logger _logger; private bool _loggerInitialized; private object _loggerLock = new object(); public Logger Logger => LazyInitializer.EnsureInitialized( ref _logger, ref _loggerInitialized, ref _loggerLock, () => new Logger("app.log"));
3. 与 Lazy 的对比
特性 | LazyInitializer.EnsureInitialized() | Lazy\<T> |
---|---|---|
内存开销 | 无额外对象分配(直接操作字段) | 需分配 `Lazy<T>` 包装器实例 |
异常缓存 | 不缓存异常(每次失败后重试) | 缓存异常(首次异常后永远抛出) |
适用类型 | 仅引用类型 | 支持值类型和引用类型 |
初始化状态跟踪 | 需手动管理 | 内置状态管理 |
4. 最佳实践
- 首选场景:
- 性能敏感且需最小化内存开销时
- 直接初始化现有字段(非新属性)
- 线程安全建议:
- 低竞争环境 → 使用重载 1 或 2
- 高并发场景 → 使用重载 3(严格单次初始化)
- 锁对象管理:
传入
null
让方法初始化锁对象:object _lock = null; // 方法内部会替换为 new object()
若需复用同一锁,提前初始化锁对象
- 避免值类型:不支持值类型(编译错误),需改用
Lazy<T>
。
5. 总结
通过引用传递和锁对象机制,LazyInitializer
提供了比 Lazy<T>
更轻量的延迟初始化方案,适用于需要精细控制初始化过程的场景。
- 优势:轻量高效、直接操作字段、灵活控制线程安全。
- 适用:引用类型的延迟初始化,性能敏感场景。
- 注意:
- 高并发环境须使用重载 3
- 需要异常重试逻辑时优先选择(与
Lazy<T>
异常缓存行为不同) - 避免用于值类型
选择建议
- 如果需要快速实现线程安全的延迟初始化且对性能要求不是极致,可优先选择
Lazy<T>
。 - 如果在性能敏感的场景下,且初始化逻辑简单,可选择
LazyInitializer.EnsureInitialized
。
总结
Lazy<T>
和LazyInitializer.EnsureInitialized
可以实现延迟初始化,按需加载资源,提升启动性能并节省内存。
Lazy<T>
提供了封装良好的延迟初始化机制,支持复杂的初始化逻辑和多种线程安全模式,适用于需要延迟加载的场景。
LazyInitializer.EnsureInitialized
则更加轻量级,直接操作字段,适用于性能敏感且需要最小化内存开销的场景。
在实际应用中,需要根据具体需求选择合适的方式。
- 需要线程安全的场景,
Lazy<T>
的默认模式(ExecutionAndPublication
)是首选; - 而对于性能敏感且初始化逻辑简单的场景,
LazyInitializer.EnsureInitialized
的重载提供了更高效的解决方案。 - 同时,开发者还需要注意异常处理、线程安全模式的选择以及避免滥用延迟初始化导致的性能问题。