I recently refreshed my knowledge on several popular design patterns, and the Singleton Design Pattern was one of them. I’d like to share some insights about it, along with code examples. If you’re trying to understand this pattern and stumbled upon my article, I hope it can serve as a helpful resource for you
By textbook definition, the Singleton Design Pattern is employed when we need a single object for purposes like caching, thread pools, etc. The initial description can be confusing, so let me simplify it: it’s all about efficiency and resource management.
If I had to explain the Singleton Design Pattern to a 6-year-old, I would say:
Imagine you have a big toy box that takes a long time to open because it’s locked with many locks. Instead of unlocking it every time you want a toy, the Singleton is like opening it once in the morning and then keeping it open all day so you can quickly grab toys whenever you want. It saves you a lot of time!
![Singleton Design Pattern]()
Now let's move to a simple code example. Suppose I have a Logger class, I called it NonSingletonLogger /This class contains a method called LogMessage that logs any text passed to it as a parameter into a file.
As you can see in the constructor, every time we create an instance of this class, it takes significant time to fetch configuration data, establish database connections, and perform other initializations. I’ve simulated this resource-intensive process by introducing a 2-second sleep in the constructor.
Given that our Logger class is immensely popular, it’s frequently invoked from various parts of the code. To replicate this behavior, I’ve created two methods, Method1() and Method2(), both of which call our in-demand Logger class.
As you can see in the code sample, in both method, the keyword ‘new’ is used every time the Logger class is used. This means every time a new instance is created, the constructor have to perform everything it needs to perform (in our case it sleeps 2 seconds) to instantiate.I introduced a stopwatch to measure the time required to complete both methods, and the result is around 4 seconds.
![Singleton Design Pattern]()
Now, let me demonstrate how we can achieve better performance using the Singleton Design Pattern. The code is as follows
Pay attention to few things
- Sealed access modifier. Using Sealed is not mandatory when creating a Singleton Design Pattern, but it is recommended. This modifier prevents inheritance, which could lead to unexpected behavior. In some scenarios, it can enhance performance. Moreover, it signals to other developers that this class shouldn’t be subclassed.
- Private constructor. A private constructor ensures that no other classes or methods can instantiate this class using the ‘new’ keyword.
- GetInstance method. This method checks if the instance variable is null. If it is, a new instance is created; otherwise, the existing one is returned. This ensures that our class remains the sole instance throughout the system.
As a result, other classes or methods aiming to use our Singleton Logger class won’t be able to utilize the ‘new’ keyword. Instead, they should call SingletonLogger.GetInstance() to access the logger instance.
To demonstrate the utility of the Singleton Design Pattern, especially in preventing resource-intensive behaviors from recurring, I invoked both methods five times more.
The result took only around 2 seconds because the resource-intensive part (within the constructor) executed just once.
![Singleton Design Pattern]()
However, in the real world, a system is seldom used by just one person. If multiple requests try to access the Logger class-implying a multi-threaded scenario-can our Singleton Design Pattern still perform efficiently? Let’s test this out.
I created a Task array of size 3 and iterated over it using a for loop. Within the loop, I call Method1(). This simulates three threads calling Method1(), which essentially means three threads are attempting to access our SingletonLogger class.
Then an error occurred:
![Singleton Design Pattern]()
The error arises due to a ‘race condition’, where multiple threads attempt to write to the file simultaneously. This highlights an important concept when working with the Singleton Design Pattern: Thread-Safety.
Before I show how to deal with the race condition, let us revisit our Singleton Constructor. Could multiple threads create several instances? Even though we’ve implemented a check with if (instance == null) before creating a new instance, what if multiple threads evaluate this condition simultaneously and find it true? Could this lead to the creation of multiple instances?
Let’s try by putting a Console.WriteLine message in the constructor, and run the main program again in a multi-threaded environment.
And you can see the results, it prints the constructor’s message 3 times!
![Singleton Design Pattern]()
This indicates a flaw in our Singleton Design Pattern. In a multi-threaded scenario, we inadvertently created three instances. Imagine the resources wasted if every constructor call consumed significant resources.
So, in order to address this problem, we need to use lock. The code implementation is like this:
Lock is a C# statement ensuring that a specific block of code cannot be executed by more than one thread simultaneously.
Referring to the above code, we’ve created an object variable named syncLock. This variable is used in conjunction with the lock statement. When a thread encounters a lock statement, it attempts to obtain an exclusive lock on the specified object. If no other thread holds the lock, it gains it and proceeds to execute the code block.
After incorporating the lock into the code and rerunning it, only a single instance was created this time.
![Singleton Design Pattern]()
Just to let you know, this method of using lock is called Double-Checked Locking pattern, since it checks twice whether the instance is null. So you learn another term in Singleton Design Pattern that can answer in an interview section.
Okay, now we have solve the only single instance can be created, how about the ‘race condition’?
The solution is pretty much the same, we also apply the lock in the LogMessage method, like this:
We create another object variable called fileLock, and use it to determine whether the method is exclusive lock when it is access by thread. Please take note that make sure you use different object variable , if you have different method that also requiring to lock checks. In this case, I used separate variables for the constructor and the LogMessage method.
Upon execution, there were no errors, and the log messages were successfully written to the text file
![Singleton Design Pattern]()
Within the topic of the Singleton Design Pattern, there are two strategies: Eager Initialization and Lazy Initialization. However, to keep this article concise, I’ve decided to address these strategies in a separate follow-up piece.
Part 2: Singleton Design Pattern: Eager And Lazy Initialization With Code Example
You may have the source code from my Github.