博客 / 詳情

返回

深入理解 System.Lazy<T>:C#.NET 延遲初始化與線程安全

什麼是 Lazy<T>

System.Lazy<T>.NET Framework 4.0 引入(位於 System 命名空間)的泛型類,用於實現線程安全的延遲初始化(Lazy Initialization)。它確保一個昂貴的對象或資源只在第一次真正需要時才被創建,並且在多線程環境下保證初始化只發生一次。

  • 核心特性:

    • 延遲計算:值的創建被推遲到第一次訪問 .Value 屬性時。
    • 線程安全:內置多種線程安全模式,默認情況下完全線程安全。
    • 異常緩存:如果初始化過程中拋出異常,後續訪問會重複拋出同一個異常(不重複執行工廠方法)。
    • 值緩存:一旦初始化完成,後續所有訪問都返回同一個實例(單例行為)。
  • 適用場景:

    • 昂貴資源初始化(如數據庫連接、大文件加載、複雜計算)。
    • 配置對象、單例模式(比雙檢查鎖更安全簡潔)。
    • 避免啓動時不必要的開銷(尤其在某些代碼路徑可能不使用該對象時)。

為什麼使用 Lazy<T>

傳統方式(如直接在字段初始化或手動雙檢查鎖)存在問題:

  • 提前初始化:浪費資源。
  • 手動鎖:易出錯(死鎖、競爭條件、ABA 問題)。
  • 異常處理複雜:需手動緩存異常。

Lazy<T>性能優化:按需初始化。

  • 線程安全:微軟高度優化,無鎖或輕量鎖實現。
  • 代碼簡潔:一行代碼取代複雜的雙檢查鎖。
  • ABA:內部使用複合狀態 + CAS 機制,徹底避免 ABA 問題。 完美解決這些問題:

如何使用 Lazy<T>

核心語法與屬性

成員 作用
Lazy<T>() 構造函數:默認使用T的無參構造創建對象,線程安全模式為ExecutionAndPublication
Lazy<T>(Func<T> valueFactory) 構造函數:自定義對象創建邏輯(支持傳參、複雜初始化)
Lazy<T>(LazyThreadSafetyMode mode) 構造函數:指定線程安全模式
Value 核心屬性:首次訪問觸發對象初始化,後續訪問返回已創建的實例(只讀)
IsValueCreated 布爾屬性:判斷對象是否已完成初始化

基礎示例(默認構造)

using System;
using System.Threading;

// 模擬創建成本高的對象(如加載配置、連接數據庫)
public class ExpensiveObject
{
    public ExpensiveObject()
    {
        Console.WriteLine("ExpensiveObject 開始初始化(耗時操作)...");
        Thread.Sleep(2000); // 模擬2秒耗時初始化
        Console.WriteLine("ExpensiveObject 初始化完成!");
    }

    public void DoWork() => Console.WriteLine("ExpensiveObject 執行業務邏輯...");
}

class LazyBasicDemo
{
    static void Main()
    {
        Console.WriteLine("程序啓動,創建Lazy<ExpensiveObject>實例...");
        // 此時僅創建Lazy<T>容器,ExpensiveObject並未實例化
        Lazy<ExpensiveObject> lazyObj = new Lazy<ExpensiveObject>();

        Console.WriteLine($"對象是否已創建:{lazyObj.IsValueCreated}"); // 輸出:False

        // 首次訪問Value:觸發ExpensiveObject的構造函數
        Console.WriteLine("\n=== 首次訪問Value ===");
        lazyObj.Value.DoWork();
        Console.WriteLine($"對象是否已創建:{lazyObj.IsValueCreated}"); // 輸出:True

        // 再次訪問Value:直接返回已創建的實例,不再初始化
        Console.WriteLine("\n=== 再次訪問Value ===");
        lazyObj.Value.DoWork();
    }
}

輸出結果:

程序啓動,創建Lazy<ExpensiveObject>實例...
對象是否已創建:False

=== 首次訪問Value ===
ExpensiveObject 開始初始化(耗時操作)...
ExpensiveObject 初始化完成!
ExpensiveObject 執行業務邏輯...
對象是否已創建:True

=== 再次訪問Value ===
ExpensiveObject 執行業務邏輯...

自定義工廠方法(帶參數初始化)

若對象需要傳參或複雜初始化邏輯,使用 Func<T> 工廠方法:

// 自定義帶參數的對象構造
public class ConfigObject
{
    private string _configPath;
    public ConfigObject(string configPath)
    {
        _configPath = configPath;
        Console.WriteLine($"加載配置文件:{configPath}");
    }

    public string GetConfig() => $"配置內容(來自{_configPath})";
}

class LazyFactoryDemo
{
    static void Main()
    {
        // 自定義工廠方法:創建ConfigObject並傳入參數
        Lazy<ConfigObject> lazyConfig = new Lazy<ConfigObject>(() => 
        {
            Console.WriteLine("執行自定義工廠方法...");
            return new ConfigObject("appsettings.json");
        });

        Console.WriteLine("首次訪問配置:");
        Console.WriteLine(lazyConfig.Value.GetConfig()); // 觸發工廠方法
    }
}

Lazy<T> 的線程安全模式

Lazy<T> 的關鍵特性是支持多線程場景,通過 LazyThreadSafetyMode 枚舉控制線程安全行為

線程安全模式 適用場景 核心行為 性能
None 單線程環境 無線程安全保障,多線程同時訪問Value可能創建多個實例 最高(無鎖)
PublicationOnly 多線程 + 對象創建成本低 多線程可同時初始化,但最終僅保留一個實例(其他實例被丟棄),無鎖阻塞
ExecutionAndPublication(默認) 多線程 + 對象創建成本高 加鎖保證只有一個線程執行初始化,其他線程等待,絕對單實例

示例:多線程下的線程安全模式

class LazyThreadSafetyDemo
{
    // 默認模式:ExecutionAndPublication(加鎖保證單實例)
    static Lazy<ExpensiveObject> lazySafeObj = new Lazy<ExpensiveObject>();

    static void AccessLazyObj()
    {
        Console.WriteLine($"線程{Thread.CurrentThread.ManagedThreadId} 嘗試訪問Value...");
        var obj = lazySafeObj.Value;
        Console.WriteLine($"線程{Thread.CurrentThread.ManagedThreadId} 獲取對象HashCode:{obj.GetHashCode()}");
    }

    static void Main()
    {
        // 啓動5個線程同時訪問
        for (int i = 0; i < 5; i++)
        {
            new Thread(AccessLazyObj).Start();
        }
        Thread.Sleep(3000); // 等待所有線程完成
    }
}

輸出結果

所有線程最終獲取的 HashCode 完全相同,且僅觸發一次初始化(證明單實例):

線程3 嘗試訪問Value...
ExpensiveObject 開始初始化(耗時操作)...
線程4 嘗試訪問Value...
線程5 嘗試訪問Value...
線程6 嘗試訪問Value...
線程7 嘗試訪問Value...
ExpensiveObject 初始化完成!
線程3 獲取對象HashCode:46104728
線程4 獲取對象HashCode:46104728
線程5 獲取對象HashCode:46104728
線程6 獲取對象HashCode:46104728
線程7 獲取對象HashCode:46104728

高級應用場景

懶加載單例模式

Lazy<T>.NET 中實現線程安全、懶加載單例的最優方式:

public sealed class LazySingleton
{
    // 私有靜態Lazy實例:延遲初始化,默認線程安全
    private static readonly Lazy<LazySingleton> _lazyInstance = new Lazy<LazySingleton>(() => new LazySingleton());

    // 私有構造函數:禁止外部實例化
    private LazySingleton() 
    {
        Console.WriteLine("單例對象初始化...");
    }

    // 公共屬性:首次訪問時創建實例
    public static LazySingleton Instance => _lazyInstance.Value;

    public void DoBusiness() => Console.WriteLine("單例對象執行業務邏輯...");
}

// 使用
class SingletonDemo
{
    static void Main()
    {
        Console.WriteLine("程序啓動,未訪問單例...");
        // 首次訪問Instance:觸發單例初始化
        LazySingleton.Instance.DoBusiness();
        // 再次訪問:直接返回已創建的實例
        LazySingleton.Instance.DoBusiness();
    }
}

處理初始化異常

Lazy<T> 的工廠方法拋出異常,後續訪問 Value 會緩存並重新拋出同一異常

Lazy<ExpensiveObject> lazyErrorObj = new Lazy<ExpensiveObject>(() =>
{
    throw new InvalidOperationException("初始化失敗:配置文件不存在!");
});

try
{
    // 首次訪問:拋出異常
    lazyErrorObj.Value.DoWork();
}
catch (AggregateException ex)
{
    Console.WriteLine($"初始化異常:{ex.InnerException.Message}");
}

try
{
    // 再次訪問:仍拋出同一異常(異常被緩存)
    lazyErrorObj.Value.DoWork();
}
catch (AggregateException ex)
{
    Console.WriteLine($"再次訪問異常:{ex.InnerException.Message}");
}

異步懶加載(偽 AsyncLazy)

private readonly Lazy<Task<ExpensiveObject>> _asyncResource = new(async () =>
{
    await Task.Delay(2000);
    return new ExpensiveObject();
});

public async Task<ExpensiveObject> GetResourceAsync() => await _asyncResource.Value;

支持刷新(Reset)的自定義包裝

public class RefreshableLazy<T>
{
    private Lazy<T> _current;

    public RefreshableLazy(Func<T> factory)
    {
        _current = new Lazy<T>(factory);
    }

    public T Value => _current.Value;

    public void Refresh()
    {
        var factory = _current.Value; // 保留舊工廠?或傳入新工廠
        _current = new Lazy<T>(_current.Factory); // 重新創建
    }
}

與依賴注入結合(ASP.NET Core)

services.AddSingleton<ExpensiveService>();
services.AddSingleton<Lazy<ExpensiveService>>(provider => 
    new Lazy<ExpensiveService>(provider.GetRequiredService<ExpensiveService>));

配置文件加載

public class AppConfig
{
    private static readonly Lazy<AppConfig> _instance = 
        new Lazy<AppConfig>(LoadConfiguration);
    
    public static AppConfig Instance => _instance.Value;
    
    public string ConnectionString { get; }
    public int Timeout { get; }
    
    private AppConfig(string config)
    {
        // 解析配置
    }
    
    private static AppConfig LoadConfiguration()
    {
        string configData = File.ReadAllText("config.json");
        return new AppConfig(configData);
    }
}

Lazy<T> vs 手動懶加載

手動實現線程安全的懶加載需要寫雙重檢查鎖定(易出錯),而 Lazy<T> 已封裝所有邏輯:

// 手動實現(線程安全,需雙重檢查+volatile)
private volatile ExpensiveObject _manualObj;
private readonly object _lockObj = new object();
public ExpensiveObject ManualObj
{
    get
    {
        if (_manualObj == null)
        {
            lock (_lockObj)
            {
                if (_manualObj == null)
                {
                    _manualObj = new ExpensiveObject();
                }
            }
        }
        return _manualObj;
    }
}

// Lazy<T>實現(一行搞定,線程安全)
private readonly Lazy<ExpensiveObject> _lazyObj = new Lazy<ExpensiveObject>();
public ExpensiveObject LazyObj => _lazyObj.Value;

Lazy<T> 內部實現原理(簡要)

  • 使用一個私有字段(如 object? _box)存儲狀態:

    • null:未初始化
    • Box<T>:已完成(包含值)
    • 異常對象:初始化失敗
  • 初始化時使用 Interlocked.CompareExchange 進行原子狀態轉換。
  • 內部狀態機結合版本機制,徹底防止 ABA 問題。
  • ExecutionAndPublication 模式下使用輕量自旋 + 等待機制,確保只有一個線程執行工廠方法。

優點與缺點

方面 優點 缺點
線程安全 內置完美支持,防 ABA、防重複初始化 None 模式下需手動注意
易用性 代碼極簡,一行搞定 不支持主動 Reset(需自定義包裝)
性能 高度優化,無鎖路徑極快 第一次訪問可能阻塞(但這是延遲初始化的本質)
功能 異常緩存、值緩存、IsValueCreated 查詢 不支持異步初始化(需用 Lazy<Task<T>>)
適用性 幾乎所有懶加載場景的首選 若需支持刷新/重置,需額外封裝

最佳實踐

  • 優先使用 Lazy<T> 替代手動雙檢查鎖。
  • 始終使用默認線程安全模式(除非明確需要 PublicationOnly)。
  • 工廠方法應無副作用(尤其在 PublicationOnly 模式下)。
  • 異常處理:捕獲 .Value 拋出的異常。
  • 不要在工廠方法中訪問同一個 Lazy 實例(可能死鎖)。

總結

  • System.Lazy<T>.NET 官方的延遲初始化工具,核心是首次訪問 Value 時創建對象,提升程序啓動速度和內存效率;
  • 核心屬性:Value(觸發初始化)、IsValueCreated(判斷是否已創建);
  • 線程安全模式需按需選擇:單線程用 None,多線程高成本對象用默認的ExecutionAndPublication
  • 最優場景:創建成本高 / 不一定使用的對象、懶加載單例模式;
user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.