6. Singleton Pattern and Race Condition
When writing code, there are times when you want to create a single instance of a variable or method and share it everywhere. There are two main approaches: defining and using static variables/methods within a class, or creating a single instance of a Singleton object.
Static Variables and Methods
The first approach is to use static variables and methods of a class without requiring object initialization. This is achieved by loading them into the Static area of memory, rather than the Heap area for dynamic parts, allowing all threads within the program to access a single variable and method. The difference between the Heap and Static areas can be understood below.
Java, JVM Memory
Java programs run on the JVM. The ‘M’ in JVM stands for Machine, implying it acts like a small OS with its own memory management system, including Garbage Collection. The memory areas are divided into three main types:
- Static Area: Holds unchanging values. (There are generally three terms referring to this area)
- It’s called the Static Area because it holds unchanging values.
- It’s also called the Class Area because it holds the Class itself before it’s instantiated into an object (during Class Loading).
- It’s sometimes referred to as the Method Area because it holds the functions of the Class before object instantiation.
- Heap Area and Stack Area: Hold changing values.
- Stack Area: Stores variables that only exist within a function block, such as ‘parameters’ or ‘local variables’.
- Heap Area: Stores objects.
The Class, which is the fundamental basis for object creation, is loaded into the Static area in bytecode form. Each time an object is created from that Class, the object and its variables/methods are generated by referencing the Class bytecode and then loaded into the Heap area. Static variables and methods exist within the Class without an object, so they are stored in the Static area.
The entity responsible for loading Classes into the Static area and creating objects is called the Classloader. If not customized, there is typically only one Classloader within the JVM. This implies that if you were to change it to have two Classloaders, static variables would be loaded into the Static area of each Classloader.
Singleton Pattern
The static variables and methods we just learned about can be summarized as follows. Understanding them based on their differences from Singleton is helpful.
Static Variables and Methods
- Class variables and methods created in the Static area.
- Immediately loaded into the Static area memory by the Classloader in bytecode form at program startup.
class Calculator {
// * Public: Can be initialized from outer
public Calculator() {} // Typo in original: Caculator -> Calculator
// * Static: sum(a, b)
public static int sum(Integer a, Integer b) { // Typo in original: Integer a, Integer a -> Integer a, Integer b. Also, added return type int
return a + b;
}
}
Singleton Variables and Methods = Single Object
- Object variables and methods created in the Heap area.
- Loaded into the Heap area as an object only when needed during program execution.
- Loading an object only when needed is called Lazy Loading, which is essentially the raison d’être of the Singleton pattern.
- If not used for a long time, it will be garbage collected later.
class Calculator {
// * Private: Cannot be initialized from outer
private Calculator() {} // Typo in original: Caculator -> Calculator
// * Non-Static: sum(a, b)
public int sum(Integer a, Integer b) { // Typo in original: Integer a, Integer a -> Integer a, Integer b. Also, added return type int
return a + b;
}
// * Singleton: Can be initialized only once using getInstance()
private static Calculator uniqueInstance;
public static Calculator getInstance() {
if (uniqueInstance == null) {
uniqueInstance = new Calculator();
}
return uniqueInstance;
}
}
The Singleton concept seems simple, but the problem arises when multiple threads simultaneously access the getInstance() function, which checks if the object already exists. If multiple threads independently determine that the object has not yet been created, they can each proceed to create multiple objects. This means that multiple Singleton objects could be created and exist.
Despite this critical issue, developers often don’t heavily concern themselves with this in practice because Singleton objects usually don’t hold internal variables or state values. In most cases, like the Calculator example above, they receive parameters and perform appropriate processing. Therefore, even if multiple Singleton objects are created, it typically doesn’t cause major problems.
However, if a Singleton object does maintain its own state, the situation changes dramatically. If two Singleton objects are viewed by different threads, a dreadful scenario of seeing completely different states can unfold. This situation, where multiple entities access a single resource, is called a Race Condition. To resolve this, a Lock mechanism – essentially saying “Please, one at a time” – must be applied. Of course, performance degradation is an accompanying side effect.
Race Condition
Since all Java objects, variables, and methods are non-blocking by default, as mentioned earlier, if multiple threads simultaneously access a single Singleton object, a problem arises where each thread may read an inconsistent state.
Let’s assume two threads simultaneously enter the getInstance() function of the Calculator class, which was used as a Singleton example. If they both enter the if (uniqueInstance == null) block at the same time, and neither thread has yet executed the subsequent line new Calculator(), both threads will independently determine that uniqueInstance is null. Then, in the next line, each thread will create a new object. In this scenario, the two threads will be using functions of their respective objects, not a single shared object. While this might not have a big impact for a simple calculation object, it leads to a terrible situation where two threads observe different states if the object is meant to share a single state.
Thread1: getInstance()
if (uniqueInstance == null) { // 2019-03-03 00:00:01
uniqueInstance = new Calculator(); // 2019-03-03 00:00:03 - Calculator object 1 created (Thread1)
Thread2: getInstance()
if (uniqueInstance == null) { // 2019-03-03 00:00:02
uniqueInstance = new Calculator(); // 2019-03-03 00:00:04 - Calculator object 2 created (Thread2)
The simplest solution to this is function-level blocking.
Function-Level Blocking - Synchronized
This technique blocks multiple threads attempting to access a function, making them wait while one thread is executing that function. Java’s synchronized keyword easily achieves this function call blocking. Now, Thread 2 must continuously wait until Thread 1 calls and finishes executing the function. This appears to eliminate the possibility of two threads calling the same function simultaneously.
class Calculator {
...
public static synchronized Calculator getInstance() {
if (uniqueInstance == null) {
uniqueInstance = new Calculator();
}
return uniqueInstance;
}
}
However, if the getInstance() function of the Singleton is more complex and takes a longer time to execute than the example logic above, other threads will have to pause for that entire duration while one thread completes its getInstance() call, leading to a performance issue. To address this, it would be better to apply blocking only to that specific variable, rather than blocking the entire function.
Variable Creation Unit Blocking - Volatile (DCL)
Since the original purpose is to share a “variable” among threads, there’s no reason to use function-level blocking and incur performance issues by making other threads idly wait during the long execution time of other logic besides the variable. Smart programmers, after much deliberation, devised blocking at the “variable” level instead of the “function” level, calling it DCL (Double Checked Locking). The reason for the “Double Checked” name can be inferred from the code below, where the null check is performed twice: once before entering the object creation logic, and again before creating it after entering the synchronized block.
- Function-Level Blocking - Add
synchronizedto the function
private static Calculator uniqueInstance;
public static synchronized Calculator getInstance() {
if (uniqueInstance == null) {
uniqueInstance = new Calculator();
}
return uniqueInstance;
}
- Variable Creation Unit Blocking - Add
volatileto the variable, addsynchronizedto the relevant block within the function
private volatile static Calculator uniqueInstance;
public static Calculator getInstance() {
if (uniqueInstance == null) {
synchronized (Calculator.class) { // Synchronize on the Class object
if (uniqueInstance == null) {
uniqueInstance = new Calculator();
}
}
}
return uniqueInstance;
}
While the existing function blocking method places synchronized on the getInstance() function, the variable creation unit blocking adds volatile to the variable and uses a synchronized block for the class within the function.
Visibility Problem
All programs and threads perform computations via the CPU, and variable values for these computations are fetched from “main memory” into the “cache” located right next to the CPU. What happens if two threads on different CPUs (in a multi-core environment) share a single Singleton object?

A single object shared by two threads is fundamentally stored in “main memory.” When each thread modifies the value on its respective CPU:
-
- First, the variable value is fetched from main memory to the cache.
-
- The CPU modifies the value in its cache.
-
- The changed value in the cache is written (synchronized) back to main memory.
If two threads access a variable’s value simultaneously, even if the first thread changes the variable’s value in its assigned CPU’s cache first, it might not have written it to main memory yet. As a result, the second thread, unaware of the change, performs its own value modification independently on its CPU. This problem of variable synchronization or inconsistency between threads, where one thread’s value update isn’t visible to another, is called the Visibility Problem.
I also recall reading an article stating that even if multiple threads run on a single CPU, JIT compiler-induced reordering at the assembly level can cause discrepancies in the variable values referenced by different threads.
DCL (Double Checked Locking) - The Meaning of Volatile
To solve the visibility problem, the volatile keyword forces consistency by ensuring that the value read from the “cache” matches the value in “main memory”. Adding the volatile keyword to a variable guarantees that when the CPU reads the variable’s value from its cache, it simultaneously reads the value from “main memory.” If one thread changes the value, it is immediately applied to main memory, allowing other threads to read the latest value.
However, if two threads fetch the same main memory value and attempt to change it, the problem still persists. Therefore, for write operations, blocking is unavoidable. The synchronized keyword is used to block functions that modify (WRITE) values. This ensures that if one thread is writing, other threads wait until the first thread finishes writing, then immediately read the value from main memory and proceed with their own write operation. Incidentally, this concept is analogous to the Isolation level, the highest level of transaction isolation.
Variable Usage Unit Blocking - Lazy Holder
I apologize to the readers of this post, but unfortunately, variable creation unit blocking does not ‘perfectly’ guarantee single instance creation. My goodness, we even considered CPU caches, what else could there be to look at? Do we need to examine things at the transistor level? There’s still one more step.
DCL guarantees the single creation of an object itself. However, a problem exists where if another thread attempts to use the variable immediately after the single object creation has just begun, it might end up using an incomplete, uninitialized object. When single creation starts, the constructor is executed via uniqueInstance = new Calculator(). If the constructor is even slightly complex, it will take some time for a fully functional object to be created. However, other threads accessing that object only recognize that uniqueInstance = new Calculator() has been executed; they don’t wait for the object to be fully formed. At this point, other threads might retrieve and use an incomplete object that hasn’t been fully initialized. This is known as the out-of-order write problem.
The solution is to ensure that the object is not just created, but completely initialized. Various methods have been proposed by even smarter programmers to guarantee this, some quite ingenious. Among them, the easiest to understand is the following:
public class Calculator {
...
private static class LazyHolder {
private static final Calculator UNIQUE_INSTANCE = new Calculator();
}
public static Calculator getInstance() {
return LazyHolder.UNIQUE_INSTANCE;
}
}
The UNIQUE_INSTANCE defined as static final is loaded by the Classloader into the Static area immediately at program startup. This combines the approach of static variables/methods. This ensures that UNIQUE_INSTANCE = new Calculator() is executed unconditionally before getInstance() is called, guaranteeing the object’s existence and full initialization.
I briefly used C# once and didn’t understand why classes were defined so complexly to define a Singleton object (as shown below). Studying the Singleton object this time helped me understand that it was meant to guarantee blocking for object creation and usage in a multi-threaded environment.
public sealed class Singleton
{
private static readonly Lazy<Singleton> lazy = new Lazy<Singleton>(() => new Singleton());
public static Singleton Instance { get { return lazy.Value; } }
private Singleton() {}
}
In reality, to understand Singleton, you only need to read the Singleton Pattern section of this post. You don’t necessarily need to rack your brain over Race Condition, Visibility Issue, DCL, Volatile, or LazyHolder concepts. However, C# users should understand why the LazyHolder syntax is used, and there’s no guarantee that you won’t need to create Singleton objects for stateful classes in other languages.