한국어 | English | 日本語
Senior Web Application Developer (8.8+ years)
Tech & Dev
engineering
Focusing on web frontend and backend development

6. Singleton Pattern and Race Condition

Singleton Comic
When writing code, there are times when you want to define or create a single variable or method and share it everywhere. There are two main solutions for this: the first is static variables/methods, and the second is the Singleton 'object' we will explore in this post.
First, we will learn about static variables and methods by understanding the differences between the Heap and Static areas within the JVM. We will then compare how a Singleton 'object' differs from these static variables/methods. Afterward, we will explore Race Conditions that occur when too many requests converge on a single object at once, and their solutions.

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:

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 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

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.

  private static Calculator uniqueInstance;
  public static synchronized Calculator getInstance() {
    if (uniqueInstance == null) {
      uniqueInstance = new Calculator();
    }
    return uniqueInstance;
  }
  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?

Race Condition

A single object shared by two threads is fundamentally stored in “main memory.” When each thread modifies the value on its respective CPU:

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.


  1. https://gampol.tistory.com/entry/Double-checked-locking%EA%B3%BC-Singleton-%ED%8C%A8%ED%84%B4
  2. http://thswave.github.io/java/2015/03/08/java-volatile.html
6. Singleton Pattern and Race Condition
Author
Aaron
Posted on
Licensed Under
CC BY-NC-SA 4.0
CC BY-NC-SA 4.0
More in this category
Recent posts
The Erosion of Conversational Muscle and Communication Styles by LLM Filters
In an era where LLM tools, which filter out conversational impoliteness and deliver refined responses, have become commonplace, are we truly engaging in more thoughtful conversations? This article examines the phenomenon of conversational ability, which should be honed through countless failures in real-time communication, degenerating due to reliance on external tools. It further explores the potential societal anxieties and shifts in generational behavioral patterns that this trend may bring.
Optimal Timing and Strategy for Salary Negotiation with Senior Candidates
Salary negotiation is more than just an exchange of figures; it's a strategic dance of psychological timing. This analysis explores why engaging in a gradual negotiation process from the initial stages of recruitment, rather than waiting until after a final offer (when candidates tend to adopt a more calculative stance), proves more efficient for companies and fosters a more honest sharing of resources.
The Limits of the Rule of Law and Human Diversity
The belief that all human actions can be regulated by a single legal system may be an act of hubris. This article offers a sharp analysis of the paradox of the rule of law faced by humanity, which, having escaped the hierarchical controls of the Middle Ages, has now embraced infinite modern freedom. It further examines the deepening social coercion and the demonization of others that arise under the guise of diversity.
토스트 예시 메세지