Overview
The Singleton pattern ensures a class has exact one instance and provides a global point of access to it.
The core challenge is: how to bypass traditional constructors and guarantee a single instance? Clients calling a class should not concern themselves with instance count restrictions—this responsibility falls on the class designer, not the consumer.
In essence, Singleton is a responsibility pattern. The single object instance becomes the sole authority for its class, centralizing all power and the duty to exercise it.
Intent
Ensure a class has only one instance and provide a global access point to it.
Real-World Example
The office of the President of the United States is a Singleton. The U.S. Constitution defines election, term limits, and succession order, guaranteeing that only one individual holds the office at any time. Regardless of who the president is, the title "President of the United States" serves as the global access point to that role.
Five Implementation Approaches
1. Simple (Non-Thread-Safe) Implementation
public class Singleton
{
private static Singleton _instance = null;
private Singleton() { }
public static Singleton GetInstance()
{
if (_instance == null)
{
_instance = new Singleton();
}
return _instance;
}
}
This approach is not thread-safe. In a multithreaded environment, two threads may both evalaute _instance == null as true, each creating a separate instance—violating the Singleton principle. Memory model inconsistencies may also cause one thread to see an uninitialized instance.
Advantages: Lazy instantiation (instence created only on first request); flexibility to add logic (e.g., subclass instantiation) inside the property method, though this may introduce unwanted dependencies.
2. Thread-Safe via Synchronization
public class Singleton
{
private static Singleton _instance = null;
private static readonly object _lock = new object();
private Singleton() { }
public static Singleton GetInstance()
{
lock (_lock)
{
if (_instance == null)
{
_instance = new Singleton();
}
return _instance;
}
}
}
This locks the entire method using a helper object, ensuring only one thread enters the critical section at a time. The first thread to acquire the lock creates the instance; subsequent threads find _instance not null and skip creation. The drawback is performance degradation due to locking on every call, even after the instance exists.
3. Double-Check Locking
public class Singleton
{
private static volatile Singleton _instance = null;
private static readonly object _lock = new object();
private Singleton() { }
public static Singleton GetInstance()
{
if (_instance == null)
{
lock (_lock)
{
if (_instance == null)
{
_instance = new Singleton();
}
}
}
return _instance;
}
}
The volatile keyword ensures proper memory barrier semantics. This implementation avoids locking on every call: only when _instance is null does the thread attempt to acquire the lock. Once inside the lock, the instance is rechecked before creation. It solves concurrency while preserving lazy instantiation and reducing overhead. Real-world applications rarely need this level of optimization; static initialization is often sufficient.
4. Static Initialization (Eager Loading)
public class Singleton
{
private static readonly Singleton _instance = new Singleton();
private Singleton() { }
public static Singleton GetInstance()
{
return _instance;
}
}
The instance is created when the class is first referenced. The runtime handles variable initialization, guaranteeing thread safety. This is the simplest and most performant approach in most scenarios. The only minor downside is reduced control over instantiation timing: you cannot use a non-default constructor or perform additional work before instantiation.
5. Lazy Initialization (Using Lazy<T>)
public class Singleton
{
private static readonly Lazy<Singleton> _lazyInstance =
new Lazy<Singleton>(() => new Singleton());
private Singleton() { }
public static Singleton GetInstance()
{
return _lazyInstance.Value;
}
}
This modern approach combines thread safety, lazy instantiation, and high performance. The Lazy<T> type ensures the instance is created on first access, using double-check locking internally (or a default thread-safe mode). It also supports optional exceptions if initialization fails.
Key Implementation Points
- Singleton restricts object creation rather than enhancing it.
- Avoid supporting serialization, as deserialization may create additional instances.
- Instance destruction is usually not a concern in garbage-collected environments; the pattern focuses solely on creation control.
- The core challenge is controlling how clients call
newon the class constructor. - It is acceptable and sometimes useful to extend Singleton to allow a small fixed number of instances (e.g., a pool of two).
Overhead
Even minor checks for instance existence (like simple null checks) incur some overhead. Static initialization eliminates this entirely.