This is a very popular Java interview question being asked ever since Java became popular! This is such a simple question that opens up the whole room for the Interviewer to ask a whole lot of follow-up questions. One can evaluate Java basics, design pattern knowledge and even expand to multi-threading / other LLD scenarios. There are three approaches to this puzzle: basic solution, thread-safe solution & optimized thread-safe solution.
The right way to approach this question is to start discussing the basic solution first and proceed to others only if you're asked to. Jumping to the best solution straight away is probably a red flag and you might end up having several follow-up questions on multi-threading. The idea here is to approach as if you haven't seen this puzzle before and navigate to better solutions understanding the need of optimizing them.
The best way is to start explaining the scenario with a simple class:
class Singleton{
private Singleton(){}
private static Singleton obj;
public static Singleton getInstance(){
if(obj==null){
obj = new Singleton();
}
return obj;
}
}
So, these are the minimum understandings:
A private constructor
A static object that holds the value of a singleton object (class level)
A static method for returning the object
One of the mistakes that candidates usually make here is to explain the use of static
variables as a workaround for thread safety. Note that static
variables are class-level variables that are shared across all threads by default. Instance variables do not require synchronization unless it is exposed to a multi-threading scenario. So, static
variables are NOT thread-safe by any means. Here, a static
variable is used just to represent a singleton object, meaning a single object is available across the application lifecycle.
Now the next question would be: " Is this thread-safe? " Of course not!
So, the very first thought would be to synchronize the method, thereby introducing an intrinsic lock. With that approach, the shared mutable state becomes atomically accessible, which is exactly what we want for thread safety:
class Singleton{
private Singleton(){}
private static Singleton obj;
public static synchronized Singleton getInstance(){
if(obj==null){
obj = new Singleton();
}
return obj;
}
}
But, what would be the problem here? We're enforcing an intrinsic lock
on the entire method. Acquiring and releasing a lock is an expensive operation. We want to ensure that we synchronize just enough to cover all mutable states. We should not synchronize too much or too less. So, there is a performance issue here. Also, the variable obj
may get cached in respective threads. So, there are still chances of race conditions
with this obj
state. We need to rethink on this too.
Here is the optimized version with the thread-safety concern addressed:
class Singleton{
private Singleton(){}
private static volatile Singleton obj;
public static synchronized Singleton getInstance(){
if(obj==null){
synchronized(this){
if(obj==null){
obj = new Singleton();
}
}
}
return obj;
}
}
Note that the use of volatile
above is to address the visibility
concern. Unlike regular variables, volatile
variables ensure that the changes are pushed into shared memory rather than persisting in the thread's local cache. Values of these variables will be read from RAM (shared memory). So, if one thread changes the value of obj
, then changes would be visible for other threads that are trying to consume obj
.
Since we are not synchronizing the entire method, multiple threads can concurrently access the method and check if the value of obj
is null or not. Threads can perform this check without acquiring the lock. Now there are below possible scenarios:
Thread A comes and observes that the value of
obj
is already set. Just return this value.Thread A comes and acquires the lock on
obj
. Change the value ofobj
if not changed already.Thread A comes and observes that Thread B currently hold the lock. Thread B finished execution and change the value of
obj
. Thread A resume the operation, and acquires the lock. Since the value ofobj
is already changed, just return the value.
One thing to note here is that a volatile
keyword is NOT a guarantee for thread safety. So, the counter question would be: "Then, why did we mention it as an optimal solution earlier?". Well, there is a catch: volatile
keywords bring thread safety when there are multiple reader threads, but only one writer thread!!
So, the optimized solution is still thread-safe as long as this is true.
So, as a final measure for thread safety, we can use an inbuilt thread-safe class from Java: such as AtomicReference
. AtomicReference
is a thread-safe class that enables us to perform atomic compound operations on multiple mutable states. We can encapsulate all mutable states into a AtomicReference
object and use it to atomically access/modify the value. So, the solution can be slightly modified to the below:
public class Singleton{
private Singleton(){}
private static AtomicReference<Singleton> obj;
public static synchronized SingleTon getInstance(){
if(obj==null){
synchronized(SingleTon.class){
if(obj==null){
// set the value of Singleton object and set here
obj = new AtomicReference<>();
}
}
}
return obj.get();
}
}
That is it for now. Stay tuned for interesting reads in the future :)