🤞🤞Double Cross

The Thread-Unsafe Singleton Surprise

TL;DR: A singleton pattern without thread safety creates race conditions and multiple instances.

#singleton#thread-safety#lazy-initialization#concurrency

The Code

csharp
1public sealed class Singleton
2{
3    private Singleton() { }
4
5    private static Singleton _instance;
6
7    public static Singleton GetInstance()
8    {
9        if (_instance == null)
10        {
11            _instance = new Singleton();
12        }
13        return _instance;
14    }
15}
16

The Prayer 🤞🤞

"It's just a Singleton, how hard can it be?" Famous last words. This code works perfectly... until you have more than one thread. Then suddenly you're crossing your fingers and hoping that Thread A and Thread B don't both check _instance == null at the exact same nanosecond. What are the odds, right? (Narrator: The odds were not in their favor.)

The Reality Check

Here's what happens in production when two threads hit GetInstance() simultaneously:

  1. Thread A checks if (_instance == null) → true
  2. Thread B checks if (_instance == null) → true (still null!)
  3. Thread A creates new Singleton()
  4. Thread B creates new Singleton() (a second one!)
  5. Now you have two "Singleton" instances. Congratulations, you've defeated the entire purpose of the pattern.

Even worse? If your Singleton manages resources (database connections, file handles, configuration), you now have resource leaks, inconsistent state, and bugs that only appear under load. Production traffic goes up, mysterious issues appear, and suddenly your "thread-safe singleton" is the prime suspect.

The Fix

C# gives you several proper ways to implement a thread-safe Singleton:

Option 1: Use Lazy (Recommended)

csharp
1public sealed class Singleton
2{
3    private static readonly Lazy<Singleton> _lazy = 
4        new Lazy<Singleton>(() => new Singleton());
5
6    private Singleton() { }
7
8    public static Singleton Instance => _lazy.Value;
9}
10

The Lazy<T> type handles all thread-safety for you. Simple, clean, bulletproof.

Option 2: Static Constructor

csharp
1public sealed class Singleton
2{
3    private static readonly Singleton _instance = new Singleton();
4    
5    // Explicit static constructor tells C# not to mark as beforefieldinit
6    static Singleton() { }
7    
8    private Singleton() { }
9    
10    public static Singleton Instance => _instance;
11}
12

The CLR guarantees static constructors are thread-safe. No locks needed.

Option 3: Double-Check Locking (If You Must)

csharp
1public sealed class Singleton
2{
3    private static Singleton _instance;
4    private static readonly object _lock = new object();
5
6    private Singleton() { }
7
8    public static Singleton GetInstance()
9    {
10        if (_instance == null)
11        {
12            lock (_lock)
13            {
14                if (_instance == null)
15                {
16                    _instance = new Singleton();
17                }
18            }
19        }
20        return _instance;
21    }
22}
23

This works, but it's verbose and easy to mess up. Just use Lazy<T> instead.

Better Yet: Don't Use Singleton At All

csharp
1// Register in your DI container
2services.AddSingleton<MyService>();
3
4// Then inject it
5public class MyClass
6{
7    private readonly MyService _service;
8    
9    public MyClass(MyService service)
10    {
11        _service = service;
12    }
13}
14

Let your dependency injection framework handle the lifetime. Testable, explicit dependencies, no global state.

Lesson Learned

Thread-unsafe lazy initialization is a race condition waiting to happen. Use Lazy<T>, static initialization, or better yet - let your DI container manage object lifetimes instead of rolling your own Singleton.