This post is focused on the evolutionary process of implementing the singleton design pattern that restricts the instantiation to one object.
We will start with a simple implementation and move onto a thread safe example using locking. A comparison is made using Lazy Initialization and we will complete the post with a loosely coupled singleton solution using Dependency Injection.
Download Source Code
Singleton Components
We will use logging as our singleton use-case. The logging interface and implementation shown
below will write to the console when the ConsoleLogger is instantiated.
public interface ILogger
{
void Log(string message);
}
public class ConsoleLogger : ILogger
{
public ConsoleLogger()
{
Console.WriteLine("{0} constructed", GetType().Name);
}
public void Log(string message)
{
Console.WriteLine(message);
}
}
Single Thread Singleton
Below is a simple implementation of a singleton.
public class SingleThreadLogger
{
private static ILogger _instance;
public static ILogger Instance
{
get
{
if (_instance == null)
{
_instance = new ConsoleLogger();
}
return _instance;
}
}
}
Calling the singleton in a single thread from the console:
static void Main(string[] args)
{
SingleThreadLogger.Instance.Log("Hello World");
SingleThreadLogger.Instance.Log("Hello World");
Console.ReadLine();
}
ConsoleLogger constructed
Hello World
Hello World
Calling the singleton using multiple threads from the console:
static void Main(string[] args)
{
Parallel.Invoke(() => SingleThreadLogger.Instance.Log("Hello World"),
() => SingleThreadLogger.Instance.Log("Hello World"));
Console.ReadLine();
}
ConsoleLogger constructed
Hello World
ConsoleLogger constructed
Hello World
Oops! Two instances of the logger was instantiated.
Thread Safe Singleton
Below is an example of a poorly implemented locking approach around the constructor of the ConsoleLogger:
public class ThreadLockConstructorLogger
{
private static ILogger _instance;
private static readonly Object _lock = new object();
public static ILogger Instance
{
get
{
if (_instance == null)
{
lock (_lock)
{
// WARNING: Multiple instantiation
_instance = new ConsoleLogger();
}
}
return _instance;
}
}
}
Calling the singleton using multiple threads from the console:
static void Main(string[] args)
{
Parallel.Invoke(() => ThreadLockConstructorLogger.Instance.Log("Hello World"),
() => ThreadLockConstructorLogger.Instance.Log("Hello World"));
Console.ReadLine();
}
ConsoleLogger constructed
Hello World
ConsoleLogger constructed
Hello World
Oops! Two instances of the logger was instantiated.
The ThreadLockConstructorLogger class has the classic double-checked locking bug. The first thread acquired the lock and is busy on line 15 while the second thread is waiting for the lock to be released on line 13. Once the second thread acquires the lock, another instance will be created on line 15.
We can solve the problem by implementing the double locking solution as shown below, where we perform another check to verify that the class has not been instantiated once the lock has been acquired.
public class ThreadLockWriteLogger
{
private static ILogger _instance;
private static readonly Object _lock = new object();
public static ILogger Instance
{
get
{
if (_instance == null)
{
lock (_lock)
{
if (_instance == null)
{
_instance = new ConsoleLogger();
}
}
}
return _instance;
}
}
}
Calling the singleton using multiple threads from the console:
static void Main(string[] args)
{
Parallel.Invoke(() => ThreadLockWriteLogger.Instance.Log("Hello World"),
() => ThreadLockWriteLogger.Instance.Log("Hello World"));
Console.ReadLine();
}
ConsoleLogger constructed
Hello World
Hello World
Success! Only one instance of the logger was created.
Lazy Instantiated Singleton
A simplified approach is to use lazy instantiation because by default, Lazy objects are thread-safe as shown below.
public class LazyLogger
{
private static readonly Lazy<ILogger> _instance =
new Lazy<ILogger>(() => new ConsoleLogger());
public static ILogger Instance
{
get { return _instance.Value; }
}
}
Calling the lazy singleton using multiple threads from the console:
static void Main(string[] args)
{
Parallel.Invoke(() => LazyLogger.Instance.Log("Hello World"),
() => LazyLogger.Instance.Log("Hello World"));
Console.ReadLine();
}
ConsoleLogger constructed
Hello World
Hello World
Success! Only one instance of the logger was created.
Dependency Injection Singleton
A singleton is considered an anti-pattern due to the following reasons:
- Hidden dependencies makes it is hard to tell what a class is dependent on when the dependencies are not explicitly defined (ie: the constructor).
- Tight coupling occurs when singletons are hardcoded into an application as a static method and unnecessary complicates mocking dependencies in automated tests.
- Single Responsibility Principle is violated since the creation of an object is mixed with the lifecycle management of the application.
Dependency Injection (DI) can remedy the problems above. Let's have a look at the example below that consists of a WriteMessageAction class with a dependency on the ILogger interface.
public interface IAction<in T>
{
void Do(T message);
}
public class WriteMessageAction : IAction<string>
{
private readonly ILogger _logger;
public WriteMessageAction(ILogger logger)
{
if (logger == null) throw new ArgumentNullException("logger");
_logger = logger;
}
public void Do(string message)
{
_logger.Log(message);
}
}
We can use Unity to manage the lifecycle of the ILogger instance as shown below.
static void Main(string[] args)
{
var container = new UnityContainer();
container.RegisterType<ILogger, ConsoleLogger>(
new ContainerControlledLifetimeManager());
container.RegisterType<IAction<string>, WriteMessageAction>("One");
container.RegisterType<IAction<string>, WriteMessageAction>("Two");
var actions = container.ResolveAll<IAction<string>>();
actions.ForEach(action => action.Do("Hello World"));
Console.ReadLine();
}
ConsoleLogger constructed
Hello World
Hello World
Success! Only one instance of the logger was created.
The key is on line 4 to 5, since we pass in "new ContainerControlledLifetimeManager()" to register the object as a singleton. By default, unity will create a new instance of an object when we omit the ContainerControlledLifetimeManager constructor parameter as shown below:
{
var container = new UnityContainer();
container.RegisterType<ILogger, ConsoleLogger>();
container.RegisterType<IAction<string>, WriteMessageAction>("One");
container.RegisterType<IAction<string>, WriteMessageAction>("Two");
var actions = container.ResolveAll<IAction<string>>();
actions.ForEach(action => action.Do("Hello World"));
Console.ReadLine();
}
ConsoleLogger constructed
ConsoleLogger constructed
Hello World
Hello World
Note: A new logger instance was created as defined in our unity registration.