The Singleton pattern is one of the first design patterns most developers learn, and one of the easiest to get subtly wrong the moment multiple threads enter the picture. A singleton that works perfectly in every test you write can still create two instances in production the moment two requests hit it at the same microsecond. Here's why that happens, and which implementations actually close the gap.

The Naive Version (and Why It Breaks)

public class ConfigManager
{
    private static ConfigManager instance;

    private ConfigManager() { }

    public static ConfigManager Instance
    {
        get
        {
            if (instance == null)
            {
                instance = new ConfigManager();
            }
            return instance;
        }
    }
}

This looks correct, and it is correct as long as only one thread ever calls Instance. The problem is the gap between the check (instance == null) and the assignment (instance = new ConfigManager()). If two threads both read instance as null before either one finishes the assignment, both will proceed to construct a new ConfigManager, and one of those instances quietly overwrites the other.

You now have two singletons that briefly existed, each potentially handed out to different parts of your application before one got discarded, which defeats the entire purpose of the pattern.

This is a classic race condition.

Fix #1: The Lock (Correct, but Costly)

The most direct fix is to guard the check-and-create with a lock:

public class ConfigManager
{
    private static ConfigManager instance;
    private static readonly object lock = new object();

    private ConfigManager() { }

    public static ConfigManager Instance
    {
        get
        {
            lock (lock)
            {
                if (instance == null)
                {
                    instance = new ConfigManager();
                }
                return instance;
            }
        }
    }
}

This is correct, only one thread can be inside the lock block at a time, so the check-and-create can never race. But it has a real downside: every single call to Instance, forever, has to acquire the lock, even long after the singleton has already been created and there's nothing left to protect.

Fix #2: Lazy<T> (The Modern, Recommended Approach)

.NET gives you Lazy<T>, which handles thread-safe, on-demand initialization for you, correctly, with none of the manual locking:

public class ConfigManager
{
    private static readonly Lazy<ConfigManager> lazyInstance =
        new Lazy<ConfigManager>(() => new ConfigManager());

    private ConfigManager() { }

    public static ConfigManager Instance => lazyInstance.Value;
}

By default, Lazy<T> uses LazyThreadSafetyMode.ExecutionAndPublication, which guarantees that the factory delegate runs exactly once, even under concurrent access, and that every thread sees the same fully-constructed instance. If multiple threads call .Value at the same time before initialization has happened, they'll block until the first one finishes.

A Note on Dependency Injection

It's also worth saying plainly: in most modern .NET applications built with a DI container, you often don't need to hand-write a singleton at all. Registering a service with AddSingleton<T>() gives you the same guarantee, one instance, created thread-safely, shared across the application, without a custom Instance property anywhere.

The patterns above are still essential to understand.