The Thread-Unsafe Singleton Surprise
TL;DR: A singleton pattern without thread safety creates race conditions and multiple instances.
The Code
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}
16The 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:
- Thread A checks
if (_instance == null)→ true - Thread B checks
if (_instance == null)→ true (still null!) - Thread A creates
new Singleton() - Thread B creates
new Singleton()(a second one!) - 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)
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}
10The Lazy<T> type handles all thread-safety for you. Simple, clean, bulletproof.
Option 2: Static Constructor
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}
12The CLR guarantees static constructors are thread-safe. No locks needed.
Option 3: Double-Check Locking (If You Must)
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}
23This works, but it's verbose and easy to mess up. Just use Lazy<T> instead.
Better Yet: Don't Use Singleton At All
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}
14Let 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.