Introduction
The Singleton Design Pattern is one of the most commonly used design patterns in C#. It ensures that a class has only one instance and provides a global access point to that instance.
However, in multi-threaded applications, creating a thread-safe Singleton becomes very important. If not handled correctly, multiple threads can create multiple instances, which breaks the purpose of Singleton.
In this article, we will learn how to implement a thread-safe Singleton using Lazy in simple words.
What is Singleton Design Pattern?
Singleton means:
Example use cases:
Basic idea:
You don’t allow users to create objects directly. Instead, you control the instance creation inside the class.
Problem with Traditional Singleton (Not Thread-Safe)
Let’s see a simple Singleton example:
public class Singleton
{
private static Singleton _instance;
private Singleton() { }
public static Singleton Instance
{
get
{
if (_instance == null)
{
_instance = new Singleton();
}
return _instance;
}
}
}
Problem:
Result:
❌ Multiple instances created
This breaks thread safety.
What is Thread Safety?
Thread safety means that your code works correctly even when multiple threads run at the same time.
In Singleton:
Traditional Fix: Locking (But Not Efficient)
private static readonly object _lock = new object();
public static Singleton Instance
{
get
{
lock (_lock)
{
if (_instance == null)
{
_instance = new Singleton();
}
return _instance;
}
}
}
Problem:
Locking is slow
Performance overhead
This is where Lazy helps.
What is Lazy in C#?
Lazy is a built-in class in .NET that ensures:
Simple meaning:
"Create the object only when someone asks for it, and do it safely."
How Lazy Solves Thread Safety
Lazy handles:
Thread synchronization internally
Ensures only one instance is created
Avoids unnecessary locking
So, we don’t need to write complex thread-safe code.
Implementing Thread-Safe Singleton Using Lazy
Here is the best and clean approach:
public sealed class Singleton
{
private static readonly Lazy<Singleton> _instance =
new Lazy<Singleton>(() => new Singleton());
private Singleton() { }
public static Singleton Instance => _instance.Value;
}
Explanation (Step-by-Step)
Let’s understand this code:
Lazy holds the instance
The lambda () => new Singleton() defines how to create it
Object is NOT created immediately
It is created only when Instance is accessed
Lazy ensures thread safety automatically
This makes the code simple, clean, and efficient.
Why Use Sealed Class?
public sealed class Singleton
Reason:
Benefits of Using Lazy Singleton
Real-World Example
Let’s create a Logger using Singleton:
public sealed class Logger
{
private static readonly Lazy<Logger> _instance =
new Lazy<Logger>(() => new Logger());
private Logger() { }
public static Logger Instance => _instance.Value;
public void Log(string message)
{
Console.WriteLine($"Log: {message}");
}
}
Usage:
Logger.Instance.Log("Application started");
Logger.Instance.Log("User logged in");
No matter how many times you call Instance, only one Logger object is created.
Common Mistakes Developers Make
Not Making Constructor Private
If constructor is public:
var obj = new Singleton();
❌ Breaks Singleton
Always make constructor private.
Not Using Lazy Initialization
Creating instance at startup wastes memory if not used.
Lazy solves this problem.
Overusing Singleton
Singleton should not be used everywhere.
Use it only when:
Overuse can lead to tight coupling.
Ignoring Thread Safety
In multi-threaded apps, ignoring thread safety can cause bugs that are hard to debug.
Always prefer Lazy or other safe approaches.
When to Use Singleton Pattern
Use Singleton when:
You need a single shared resource
Configuration should be consistent
Logging should be centralized
Avoid when:
Summary
The Singleton Design Pattern ensures that only one instance of a class exists. However, in multi-threaded applications, making it thread-safe is crucial. Using Lazy in C# is the best and simplest way to implement a thread-safe Singleton. It provides built-in thread safety, lazy initialization, and cleaner code without using locks. By understanding this approach, developers can build efficient, scalable, and reliable .NET applications.