class NonThreadSafeSingleton { static NonThreadSafeSingleton instance; public static NonThreadSafeSingleton Instance { get { if (instance == null) instance = new NonThreadSafeSingleton(); return instance; } } }
The above implementation of the Singleton pattern may work, but it doesn't make it safe to use between multiple threads. If multiple threads were to access the above Singleton implementation we may experience the following sequence as executed by the runtime:
Order | Thread 1 | Thread 2 |
1 | if (instance == null) | |
2 | if (instance == null) | |
3 | instance = new NonThreadSafeSingleton(); | |
4 | instance = new NonThreadSafeSingleton(); | |
5 | return instance; | |
6 | return instance; |
As you can see in the order of execution the NonThreadSafeSingleton class is instantiated twice due to a race-condition. When in reality our desired execution is as follows:
Order | Thread 1 | Thread 2 |
1 | if (instance == null) | |
2 | instance = new NonThreadSafeSingleton(); | |
3 | return instance; | |
4 | return instance; |
If we're going to make this thread-safe one approach in .NET 2.0 would be to use an exclusive lock:
class ThreadSafeSingletonWithLock { static ThreadSafeSingletonWithLock instance; static object locker = new Object(); public static ThreadSafeSingletonWithLock Instance { get { lock (locker) { if (instance == null) instance = new ThreadSafeSingletonWithLock(); } return instance; } } }
But looking at the sequence of execution we now run into the problem of a lock always being acquired even if the instance is already constructed (executions 1 and 6 listed below):
Order | Thread 1 | Thread 2 |
1 | lock (locker) | |
2 | if (instance == null) | |
3 | instance = new ThreadSafeSingletonWithLock(); | |
4 | return instance; | |
5 | lock (locker) | |
6 | return instance; |
This causes an additional overhead which isn't desired. It is actually possible to achieve the same requirement of synchronising the object construction whilst minimising the amount of locking required:
class ThreadSafeSingletonWithDoubleCheckLock { static ThreadSafeSingletonWithDoubleCheckLock instance; static object locker = new Object(); public static ThreadSafeSingletonWithDoubleCheckLock Instance { get { if (instance == null) { lock (locker) { if (instance == null) instance = new ThreadSafeSingletonWithDoubleCheckLock(); } } return instance; } } }
It is still possible for two threads to "lock" during object construction, although if the object construction is quick the likelihood of this condition occurring is minimised. Any additional instance retrievals after instantiation are done outside of the lock providing an improvement during runtime. Although the above implementation of the thread-safe Singleton pattern is better than the prior implementation, it kind of feels a bit messy having to implement the condition to check if the instance is equal to null two times, an inexperienced developer may think this is unnecessary and run the risk of the line of code being removed.
An even better implementation is provided in .NET 4.0 BCL with Lazy<T>. This allows a single instance of a class to be instantiated upon request in a thread-safe way. An example of it's usage is shown below:
class ThreadSafeLazySingleton { readonly static Lazy<ThreadSafeLazySingleton> instance = new Lazy<ThreadSafeLazySingleton>(() => new ThreadSafeLazySingleton()); private ThreadSafeLazySingleton(){} public static ThreadSafeLazySingleton Instance { get { return instance.Value; } } }
The Lazy<T> overloaded constructors provide performance options during construction. For example, if the instance isn't going to be used between multiple threads then it is possible to define the Lazy instance as non-thread safe to ensure the highest possible performance.
By using the Lazy class we achieve the following:
- Encapsulating the locking mechanism: As the synchronisation of instantiation is internal to the Lazy<T> class any future optimisations done by the .NET team should not effect the usage of this class unless they intend of implementing a breaking-change;
- Allows the singleton instance to be used in a thread-safe environment;
- Gracefully encapsulates the double-check locking inside the class without having the developer to implement this themselves