Thread-Safe Singleton
Part of the Object Oriented Programming section of Coddy's C# journey — lesson 52 of 70.
The Singleton pattern ensures a class has only one instance throughout your application and provides a global access point to it. This is useful for managing shared resources like configuration settings, logging services, or database connections.
A basic Singleton uses a private constructor to prevent external instantiation and a static property to provide access:
public class Logger
{
private static Logger _instance;
private static readonly object _lock = new object();
private Logger() { } // Private constructor
public static Logger Instance
{
get
{
lock (_lock)
{
if (_instance == null)
_instance = new Logger();
return _instance;
}
}
}
public void Log(string message) => Console.WriteLine(message);
}The lock statement ensures thread safety - if multiple threads try to access Instance simultaneously, only one can enter the critical section at a time. Without this, two threads could both see _instance as null and create separate instances.
C# offers a simpler thread-safe approach using static initialization:
public class Logger
{
private static readonly Logger _instance = new Logger();
private Logger() { }
public static Logger Instance => _instance;
public void Log(string message) => Console.WriteLine(message);
}This version is thread-safe because the CLR guarantees static field initialization happens only once. Both approaches ensure you always get the same instance:
Logger.Instance.Log("First call");
Logger.Instance.Log("Same instance");Challenge
EasyLet's build a thread-safe configuration manager using the Singleton pattern. In real applications, configuration settings need to be accessible throughout the entire program while ensuring only one instance exists - a perfect use case for Singleton.
You'll organize your code across two files:
ConfigManager.cs: Create aConfigManagerclass in theConfigurationnamespace that implements the Singleton pattern. Your configuration manager should:- Have a private static field to hold the single instance
- Use a private constructor to prevent external instantiation
- Provide a static
Instanceproperty that returns the single instance (use the simpler static initialization approach for thread safety) - Include a private string field
_appNameinitialized to"MyApp" - Have a
SetAppName(string name)method to update the app name - Have a
GetAppName()method that returns the current app name
Program.cs: In your main file, demonstrate that the Singleton works correctly by accessing the instance multiple times and showing that changes persist across all references.
You will receive one input:
- A new application name to set (e.g.,
ProductionServer)
In your program:
- Get the
ConfigManagerinstance and print the default app name - Get the instance again (into a different variable) and use it to set the new app name from input
- Get the instance a third time and print the app name to prove all references share the same data
For example, if the input is ProductionServer, the output should be:
MyApp
ProductionServerThis demonstrates the core benefit of Singleton - no matter how many times you access Instance, you always get the same object with shared state. The static initialization approach guarantees thread safety because the CLR ensures the static field is initialized only once, even if multiple threads access it simultaneously.
Cheat sheet
The Singleton pattern ensures a class has only one instance throughout your application and provides a global access point to it.
Basic Singleton with explicit locking for thread safety:
public class Logger
{
private static Logger _instance;
private static readonly object _lock = new object();
private Logger() { } // Private constructor
public static Logger Instance
{
get
{
lock (_lock)
{
if (_instance == null)
_instance = new Logger();
return _instance;
}
}
}
public void Log(string message) => Console.WriteLine(message);
}The lock statement ensures thread safety by allowing only one thread to enter the critical section at a time.
Simpler thread-safe Singleton using static initialization:
public class Logger
{
private static readonly Logger _instance = new Logger();
private Logger() { }
public static Logger Instance => _instance;
public void Log(string message) => Console.WriteLine(message);
}This approach is thread-safe because the CLR guarantees static field initialization happens only once.
Usage - always returns the same instance:
Logger.Instance.Log("First call");
Logger.Instance.Log("Same instance");Try it yourself
using System;
using Configuration;
class Program
{
public static void Main(string[] args)
{
// Read input - the new application name
string newAppName = Console.ReadLine();
// TODO: Get the ConfigManager instance and print the default app name
// TODO: Get the instance again (into a different variable) and use it to set the new app name
// TODO: Get the instance a third time and print the app name to prove all references share the same data
}
}
This lesson includes a short quiz. Start the lesson to answer it and track your progress.
All lessons in Object Oriented Programming
1Fundamentals of OOP
External FilesNamespaces & DirectivesIntro to Classes & ObjectsThe 'this' KeywordMethods and ParametersFields vs PropertiesConstructorsObject InitializersRecap - Simple Calculator4Inheritance
Basic Inheritance (:) SyntaxThe 'base' KeywordVirtual & Override KeywordsSealed ClassesThe 'object' Base ClassRecap - Employee Hierarchy7Advanced Features
Operator OverloadingIndexers (this[])ToString() OverrideExtension MethodsRecap - Custom List10Design Patterns Part 1
Intro to Design PatternsThread-Safe SingletonFactory PatternObserver Pattern (Events)Strategy Pattern2Properties & Static Members
Auto-Implemented PropertiesRead/Write-Only PropertiesStatic Fields & MethodsStatic ClassesExpression-Bodied Members5Polymorphism & Interfaces
Compile vs Runtime PolyInterface vs Abstract ClassMultiple InterfacesExplicit InterfacesUpcasting & DowncastingRecap - Shape Calculator