The singleton design pattern is often considered the easiest to understand. Sometimes it is required that the Java Virtual Machine only held one instance of a certain class. In object-oriented design, the singleton design pattern is implemented so that there is only one instance of the desired class per JVM at a particular time.
Previously, we looked at singleton design patterns that were not thread-safe. However, in the real world, we multitask, and resources are shared among individuals who work on them simultaneously. For example, one team of engineers works on upgrading the database, and another team updates the records. This obviously can't be done simultaneously, and if there is miscommunication between the teams, the situation will turn into a developer's nightmare!
To solve this problem, we can use a thread-safe singleton design pattern. It seems intuitive to use a synchronized block to implement a thread-safe pattern, but this approach can be summarised with a quote from an unknown Java programmer:
I achieved thread safety but at the cost of performance, an increase in average wait time, and a dissatisfying user experience. I am both happy and unhappy.
In other words, there is always a trade-off between thread safety and performance.
Common properties of the Singleton class
Here are the main things to know about the Singleton class:
-
One cannot create a direct object of the class, which can be achieved by making class constructors private.
-
We use the
getInstance()static factory method to return the object reference which is stored using a static variable (try to figure out why we use a static variable here).
Let's move on and see various approaches aimed at achieving thread safety!
Static field initialization
Consider a hypothetical yet simple way to launch an operating system. When the user powers up the system, there is a booting process that loads the OS into the main memory. This process ensures that all system resources are available to the OS. Now consider that the following code is used to ensure that our OS is booted as soon as BootSystem is loaded (or we can say when the machine is powered) so that we can play our favorite game once our machine is up & running!
public class BootSystem {
// BootOS object is created as soon as the class is loaded.
private static BootSystem bootSystemObject = new BootSystem();
private BootSystem() {}
// Other utility methods and variables
}
Due to the fact that BootSystem will be initialized only once by the class loader, the above implementation is guaranteed to be thread-safe. An advantage of using eager implementation is that it guarantees object creation and global availability throughout the application, right from the point of initialization. It also helps save system resources when object creation is time-consuming and resource-intensive, hampering the user experience compared to keeping the object in heap memory.
Now let's see the disadvantages. Consider loading and creating a singleton object that takes 5-10 seconds for pre-computation, and your industry standard is 3 seconds. Isn't that too slow for your business? What is more, if there is an error while creating an object, we won't be able to handle exceptions, and the application could crash.
Implementing any design pattern is a trade-off between readability, performance, and ease of future enhancement in the codebase. It's up to the developer to come up with the most appropriate and optimized solution to the problem based on their wisdom, experience, and demands.
Static block initialization
This is similar to eager initialization. Additionally, it supports exception handling through static blocks. The static block only gets called once, when the class itself is initialized.
public class Singleton{
private static Singleton singletonObject;
static {
try {
singletonObject = new Singleton();
} catch (IOException e) {
throw new RuntimeException("Bruh we have a problem!☹️", e);
}
}
private Singleton() {}
// Other utility methods & variables
}
Although we've prevented our app from crashing, the issue of slow loading still persists. To solve this, we will now look at something called double-checked locking.
Double-checked locking
It is a thread-safe implementation of lazy initialization. The implementation ensures that the object is created only when it is required. Consider the following code which has the keyword volatile and a synchronized block nested inside the if-condition.
public class Singleton {
private static volatile Singleton singletonObject;
public static Singleton getInstance() {
if (singletonObject == null) { // outer if condition
synchronized(Singleton.class) {
if (singletonObject == null) { // inner if condition
singletonObject = new Singleton();
}
}
}
return singletonObject;
}
// Other utility methods & variables
}
-
The
volatilekeyword means the changes made by one thread are instantly visible to all the other threads accessing the variable. Here it will ensure that all threads are working with updated values. -
A synchronized block in layman's terms means that if thread A is accessing
singletonObject, all other threads have to wait till thread A has finished processingsingletonObject.
Could you guess why we nested synchronized blocks inside the outer if-condition and not the other way around (give your brain a bit of exercise)? First, to prevent additional overhead. If we had done it the other way around, then every time the getInstance() method was called, it would be synchronized and block all other threads. Isn't it what we want? It is, but this would devalue the purpose of using the volatile keyword and negatively affect the application. When the instance method is first called, it will be called if the condition is true and all other threads in the meantime will have to wait. This will ensure that no partial or null reference is accessed. In subsequent accesses, its reference with updated values (due to the volatile keyword) will be returned as outer if the condition is always false.
Before discussing the next two approaches, keep in your curious mind that the next two approaches support thread safety by default (much like eager & static block initialization) because of the way they are implemented by almighty Java Developers.
Enum-based
Enums are guaranteed to have thread safety by the JVM during object initialization. Also, the amount of code that is required to create a singleton class is reduced due to enum types. Just like this:
public enum EnumSingletonClass {
singletonInstance
// As opposed to
// static final Singleton = new Singleton();
// + Much more benefits
}
On-demand holder
Consider the following Java class:
public class Singleton {
public static Singleton getInstance() {
return NestedSingletonHelper.singletonObject;
}
// Other utility methods & variables
// Private Class
private static class NestedSingletonHelper{
public static Singleton singletonObject = new Singleton(); // 1
}
}
We will not dive deep into the principles and the way the following code is handled by the JVM. However, for now, you need to remember the following points:
-
The above approach combines the principles of eager and lazy initialization.
-
NestedSingletonHelperwill not be loaded until it's referenced, which will happen when thegetInstance()method is called for the first time. This principle is based on the theory of lazy initialization. -
On the first and only call, the line commented with "1" will be executed, and on the next call, it will return a reference to the
singletonObject. This principle is based on how the classes are loaded by JVM during runtime.
Usually, this approach is considered the most effective way to create a singleton.
Conclusion
This was all about making a singleton class thread-safe. It could seem overwhelming at first, but it's one of the simplest and beginner-friendly design patterns which is quite famous in the Java community. Broadly speaking, the two ways to implement the pattern are eager & lazy initialization, while all others extend their advantages and improve their weaknesses. The singleton pattern helps save system resources, as an object is not created at each request and a single instance is reused over and over upon request. The pattern is most applicable to multi-threaded and database applications, as well as caching, logging, and configuration modules.