-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C#
-
Section 4: Working with Data
-
Section 5: Error Handling
-
Section 6: Delegates and Events
-
Section 7: Async Programming
-
Section 8: More Language Features
-
Section 9: File I/O and Serialization
-
Section 10: Networking in .NET
-
Section 11: The .NET Ecosystem
-
Section 12: Memory and Performance
-
Section 13: Concurrency Beyond Async
-
Section 14: Reflection and Attributes
-
Section 15: Testing and Best Practices
-
Section 16: Design Patterns in C#
-
Section 17: Standard Library Deep Dive
-
Section 18: Data Structures and Algorithms in C#
-
Section 19: GUI and Desktop Development Overview
-
Section 20: Practical Projects
-
Section 21: More Practice Exercises
-
Section 22: More Standard Library and Text Processing
-
Section 23: More Design Patterns
-
Section 24: More Projects
-
Section 25: Interview Practice
-
Section 26: C# Keywords Reference (Modifiers)
-
Section 27: C# Keywords Reference (Statements)
-
Section 28: C# Keywords Reference (Operators)
-
Section 29: BCL: System.Collections.Generic
-
Section 30: BCL: System.Linq
-
Section 31: BCL: System.Threading
-
Section 32: BCL: System.IO
-
Section 33: BCL: System.Text and System.Text.Json
-
Section 34: BCL: System.Net.Http
-
Section 35: C# Language Specification Topics
-
Section 36: More Practice Exercises
-
Section 37: More Async Patterns
-
Section 38: More BCL: System.Reflection and System.Diagnostics
-
Section 39: Nullable Reference Types In Depth
-
Section 40: C# Records and Pattern Matching In Depth
-
Section 41: Dependency Injection Deep Dive
-
Section 42: More Interview and Whiteboard Practice
243: Whiteboard Practice: Writing Thread-Safe Singleton Implementations
If you're prepping for a technical interview, the Singleton pattern is a classic "whiteboard" request. But here is where most candidates trip up: they assume that as long as they have a lock statement somewhere in their GetInstance method, they've solved the thread-safety problem. I've seen senior devs make this mistake. They think "safe" equals "optimal," but in a high-throughput system, a naive lock is a performance killer.
Thinking a Simple Lock is Enough
Let's look at a common implementation for a GlobalCacheManager. The goal is to ensure we only ever have one cache instance across the entire application to save memory and keep data consistent.
public sealed class GlobalCacheManager
{
private static GlobalCacheManager _instance;
private static readonly object _lock = new object();
private GlobalCacheManager() { }
public static GlobalCacheManager Instance
{
get
{
lock (_lock) // The "Naive Lock"
{
if (_instance == null)
{
_instance = new GlobalCacheManager();
}
return _instance;
}
}
}
}
Technically, this is thread-safe. You won't end up with two caches. But look at the cost. Every single time your application needs to access the cache—which could be thousands of times per second in a web API—it has to acquire a lock. Once the instance is created, there is absolutely no reason to keep locking. You've effectively turned your high-performance cache into a bottleneck where every thread has to wait in line just to read a reference that never changes again.
Optimizing with Double-Checked Locking
To fix this, we use "Double-Checked Locking." The idea is to check if the instance is null before we enter the lock. If it's already initialized, we skip the lock entirely. If it is null, we enter the lock and check again (because another thread might have initialized it in the millisecond it took us to acquire the lock).
public sealed class GlobalCacheManager
{
private static GlobalCacheManager _instance;
private static readonly object _lock = new object();
private GlobalCacheManager() { }
public static GlobalCacheManager Instance
{
get
{
if (_instance == null) // First check (no lock)
{
lock (_lock)
{
if (_instance == null) // Second check (locked)
{
_instance = new GlobalCacheManager();
}
}
}
return _instance;
}
}
}
This is significantly faster. Now, 99.9% of calls bypass the lock entirely. If you're in a whiteboard interview, this is usually the "gold standard" answer they are looking for because it proves you understand concurrency and contention. However, in actual production C# code, I rarely write this by hand anymore.
Letting the Framework Handle It with Lazy<T>
C# provides a type called Lazy<T> that handles all of this boilerplate for you. It's not just cleaner; it's guaranteed to be thread-safe by default and is highly optimized by the .NET team. I always prefer this approach because it removes the possibility of human error (like forgetting the second null check).
public sealed class GlobalCacheManager
{
// Lazy handles the thread-safety and lazy initialization internally
private static readonly Lazy<GlobalCacheManager> _lazy =
new Lazy<GlobalCacheManager>(() => new GlobalCacheManager());
public static GlobalCacheManager Instance => _lazy.Value;
private GlobalCacheManager()
{
// Initialize your cache here
}
}
When you call _lazy.Value, the runtime checks if the object exists. If it doesn't, it runs the factory method we provided. If multiple threads hit that line at once, Lazy<T> ensures only one thread creates the object while the others wait. It's elegant, concise, and less prone to bugs.
📋 Practical Task
Fixing the Thread-Safety of the AppSettingsRegistry
You have inherited a codebase where a AppSettingsRegistry is used to hold global configuration. The current implementation is not thread-safe, and the team is reporting intermittent NullReferenceExceptions and duplicate initialization logs during application startup.
Your Task: Rewrite the AppSettingsRegistry class to be thread-safe. You may choose either the Double-Checked Locking pattern or the Lazy<T> pattern, but the final implementation must ensure that the constructor is called exactly once, even when accessed by multiple concurrent threads.
public sealed class AppSettingsRegistry
{
private static AppSettingsRegistry _instance;
// The constructor currently logs a message when it runs
private AppSettingsRegistry()
{
Console.WriteLine("Initializing AppSettingsRegistry...");
}
public static AppSettingsRegistry Instance
{
get
{
// TODO: Implement thread-safe singleton logic here
if (_instance == null)
{
_instance = new AppSettingsRegistry();
}
return _instance;
}
}
public string GetSetting(string key) => $"Value for {key}";
}There are no comments for now.