Thread-Safe Singleton in C# with LazyThreadSafetyMode
Implementing a robust, thread-safe singleton pattern in modern C# is
most effectively achieved using the built-in Lazy<T>
type configured with LazyThreadSafetyMode. This article
breaks down the mechanics of the LazyThreadSafetyMode
enumeration, compares its different options, and provides a clear,
production-ready implementation of a thread-safe singleton.
Understanding LazyThreadSafetyMode
The Lazy<T> class provides lazy initialization,
deferring the creation of an object until it is first requested. When
using Lazy<T> across multiple threads, the
LazyThreadSafetyMode enum determines how thread
synchronization is handled:
ExecutionAndPublication: Only a single thread is permitted to execute the initialization factory method. All other threads block until the initialization completes, ensuring that the initialization delegate runs exactly once and all threads share the resulting instance.PublicationOnly: Multiple threads can execute the initialization delegate concurrently if they access the instance simultaneously. However, only the first thread to complete initialization successfully publishes its instance; the other instances are discarded. This avoids locks at the cost of potential redundant object allocations.None: No thread synchronization is performed. If accessed across multiple threads, behavior is undefined and race conditions will occur.
For the vast majority of singleton implementations,
LazyThreadSafetyMode.ExecutionAndPublication
is the correct choice because it guarantees that the constructor or
factory function is called only once.
Implementing the Singleton Pattern
To build a thread-safe singleton, declare a private static readonly
Lazy<T> field, specify a private constructor to
prevent external instantiation, and expose a public static property to
access the instance.
public sealed class SingletonService
{
private static readonly Lazy<SingletonService> _instance =
new Lazy<SingletonService>(
() => new SingletonService(),
LazyThreadSafetyMode.ExecutionAndPublication
);
// Private constructor prevents external instantiation
private SingletonService()
{
// Initialization logic here
}
// Public accessor
public static SingletonService Instance => _instance.Value;
public void ExecuteOperation()
{
// Business logic here
}
}Key Considerations
- Default Behavior: If you construct
new Lazy<T>(() => new T(), true), the booleantruedefaults internally toLazyThreadSafetyMode.ExecutionAndPublication. Explicitly passing the enum value improves code clarity and intent. - Exception Handling: When using
ExecutionAndPublication, if the initialization delegate throws an unhandled exception, that exception is cached. Subsequent attempts to readValuewill immediately rethrow the same exception without attempting initialization again. - Double-Checked Locking: Using
Lazy<T>eliminates the need to write custom double-checked locking logic with explicitlockstatements, reducing the risk of subtle concurrency bugs and volatile read/write mistakes.