Implementing Thread-Safe Singletons in C# Using Lazy<T>
Learn how to implement a thread-safe Singleton in C# using Lazy<T> to avoid race conditions and manual locking during shared resource initialization.
14 May 2026, 04:56 UTC

The Problem: Race Conditions in Resource Initialization
When managing a shared resource—such as a configuration cache or a database connection pool—multiple threads often attempt to initialize the resource simultaneously. A naive implementation of a Singleton pattern can lead to multiple instances being created, causing memory leaks or corrupted state. While manual double-check locking was once the standard, it is error-prone and verbose.
The most efficient way to ensure a single, thread-safe instance in modern C# (.NET 4.0+) is using the Lazy<T> type. This approach delegates the synchronization logic to the .NET Runtime, ensuring that the resource is created only once and only when first accessed.
The Smallest Suitable Design
To implement this, you need a private constructor to prevent external instantiation and a static Lazy<T> wrapper. The Lazy<T> type is thread-safe by default, meaning it handles the locking internally when the Value property is accessed.
public sealed class SharedResourceManager
{
// The Lazy<T> wrapper ensures thread-safe, deferred initialization
private static readonly Lazy<SharedResourceManager> _instance =
new Lazy<SharedResourceManager>(() => new SharedResourceManager());
// Private constructor prevents the 'new' keyword from being used externally
private SharedResourceManager()
{
// Initialize expensive resources here
Console.WriteLine("Resource Initialized");
}
// Public access point for the singleton instance
public static SharedResourceManager Instance => _instance.Value;
public void DoWork()
{
Console.WriteLine("Performing action with shared resource.");
}
}
Trust and Data Boundaries
The boundary of trust in this design is the private constructor. By sealing the class (using the sealed keyword), you prevent inheritance from bypassing the singleton logic. The data boundary is encapsulated within the Lazy<T> object; the actual instance of SharedResourceManager does not exist in memory until a thread calls Instance for the first time.
Operational Checks and Verification
To verify that the implementation is truly thread-safe and that only one instance is created, you can use a Parallel.For loop to simulate concurrent access. Run this in a console application using the .NET SDK.
// Run this in a Main method to verify singleton behavior
Parallel.For(0, 100, i =>
{
var instance = SharedResourceManager.Instance;
// Verify that the instance reference is identical across all threads
});
Expected Result: The message "Resource Initialized" should appear exactly once in the console, regardless of the number of threads in the loop.
Failure Modes and Risks
- Recursive Initialization: If the constructor of the singleton attempts to access
SharedResourceManager.Instance, the runtime will throw aSystem.InvalidOperationExceptionbecause the object is still being created. - Global State Contamination: Because the instance persists for the lifetime of the AppDomain, any state stored within the singleton persists across different business operations. This can lead to "leaky" tests where one unit test affects the outcome of another.
- Tight Coupling: Classes that call
SharedResourceManager.Instancedirectly become difficult to mock during testing.
When to Change This Design
The Lazy<T> singleton is appropriate for static, immutable, or globally shared resources. However, you should move away from this pattern in the following scenarios:
- Dependency Injection (DI): If you are using a DI container (like the built-in .NET Core service provider), register the class as a
SingletoninProgram.csinstead. This allows you to inject the dependency via constructors, improving testability. - Dynamic Reconfiguration: If the resource needs to be destroyed and recreated (e.g., when a configuration file changes), a static singleton is unsuitable because it cannot be easily reset.
- Lifecycle Management: If the resource needs to be disposed of explicitly via
IDisposablebefore the application exits, a static singleton can be problematic as there is no clear "owner" to callDispose().
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.