How to Implement Thread-Safe Singleton in C

The Singleton design pattern ensures that a class has only one instance throughout the application lifecycle and provides a global access point to that instance. In a multi-threaded C# application, naive implementations can cause race conditions, resulting in multiple instances being created simultaneously. This article explains the most reliable and thread-safe ways to implement a Singleton in C#, focusing on modern idiomatic approaches such as Lazy<T>, static initialization, and classic double-checked locking.

Common Requirements for a C# Singleton

Regardless of the thread-safety mechanism chosen, every C# Singleton must satisfy three basic criteria:

  1. A private, parameterless constructor: Prevents other classes from instantiating it with the new operator.
  2. A sealed class: Prevents subclassing, which could lead to multiple instances.
  3. A public static property: Provides global access to the single instance.

The cleanest, most idiomatic, and inherently thread-safe approach in modern C# (.NET 4.0 and later) is using the System.Lazy<T> type. It handles thread safety, lazy evaluation, and exception caching automatically behind the scenes.

public sealed class Singleton
{
    private static readonly Lazy<Singleton> _instance = 
        new Lazy<Singleton>(() => new Singleton());

    private Singleton()
    {
    }

    public static Singleton Instance => _instance.Value;
}

By default, Lazy<T> uses LazyThreadSafetyMode.ExecutionAndPublication, ensuring only one thread can execute the factory method and initialize the instance.


2. Static Initialization (Eager Loading)

The Common Language Runtime (CLR) guarantees that static constructors and field initializers are executed in a thread-safe manner before the class is accessed.

public sealed class Singleton
{
    private static readonly Singleton _instance = new Singleton();

    // Explicit static constructor to tell C# compiler
    // not to mark type as beforefieldinit
    static Singleton()
    {
    }

    private Singleton()
    {
    }

    public static Singleton Instance => _instance;
}

This pattern is simple and thread-safe without explicit lock statements. However, the instance is instantiated as soon as the class is referenced, making it less suitable if initialization is resource-intensive and needs to be deferred until explicitly required.


3. Double-Checked Locking

Double-checked locking avoids acquiring a lock every time the instance is accessed, locking only during the initial creation.

public sealed class Singleton
{
    private static Singleton? _instance;
    private static readonly object _lock = new object();

    private Singleton()
    {
    }

    public static Singleton Instance
    {
        get
        {
            if (_instance == null)
            {
                lock (_lock)
                {
                    if (_instance == null)
                    {
                        _instance = new Singleton();
                    }
                }
            }
            return _instance;
        }
    }
}

While effective, this approach is more verbose and prone to implementation errors compared to Lazy<T>.


Conclusion

For almost all modern C# applications, Lazy<T> is the preferred solution because it combines true lazy evaluation, guaranteed thread safety, and minimal boilerplate code. Use static initialization if eager loading is acceptable, and avoid manual locking mechanisms unless maintaining legacy codebases.