DEV Community

DevCorner
DevCorner

Posted on

Thread-Safe Singleton in Java: Understanding `volatile` and Double-Checked Locking

Introduction

The Singleton pattern ensures that only one instance of a class is created and provides a global access point to it. While implementing a thread-safe singleton in Java, one common and efficient approach is double-checked locking. However, to make this work correctly, we need to use the volatile keyword.

In this blog, we’ll explore:

✅ Why we need a thread-safe singleton

✅ How to implement double-checked locking

✅ The role of volatile in preventing race conditions

✅ When to use volatile in Java


Why Do We Need a Thread-Safe Singleton?

Imagine a scenario where multiple threads are trying to create an instance of a singleton class. If not handled properly, two threads might end up creating separate instances, breaking the Singleton pattern.

A naive singleton implementation might look like this:

public class Singleton {
    private static Singleton instance;  // Shared instance

    private Singleton() {}  // Private constructor to prevent instantiation

    public static Singleton getInstance() {
        if (instance == null) {  // Check if instance exists
            instance = new Singleton();  // Create instance
        }
        return instance;
    }
}
Enter fullscreen mode Exit fullscreen mode

Problem with This Approach

If two threads call getInstance() simultaneously when instance == null, both threads could create separate instances, violating the Singleton pattern.


Using Double-Checked Locking for Thread Safety

To fix this, we use synchronized, but a naive synchronization approach slows down performance:

public class Singleton {
    private static Singleton instance;

    private Singleton() {}

    public static synchronized Singleton getInstance() {  // Synchronizing entire method
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}
Enter fullscreen mode Exit fullscreen mode

Why Is This Not Ideal?

🔴 Unnecessary synchronization overhead – After the instance is created, all threads still have to go through the synchronized method, affecting performance.

✅ Solution: Double-Checked Locking

This technique reduces synchronization overhead while ensuring thread safety.

public class Singleton {
    private static volatile Singleton instance;  // Volatile ensures proper visibility

    private Singleton() {}  

    public static Singleton getInstance() {
        if (instance == null) {  // First check (without lock)
            synchronized (Singleton.class) {  // Locking only when needed
                if (instance == null) {  // Second check (inside lock)
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}
Enter fullscreen mode Exit fullscreen mode

The Role of volatile in Thread Safety

Now, let's focus on why volatile is crucial here.

Understanding Instruction Reordering

Without volatile, the JVM might reorder the instructions when initializing the singleton.

What Happens Without volatile?

During instance creation:

  1. Allocate memory for the singleton
  2. Assign reference to instance (but object is not fully initialized yet!)
  3. Call constructor to complete initialization

The compiler may swap step 2 and 3 for performance optimization. If Thread A is in the middle of creating the instance and Thread B checks instance != null, it may access an incomplete object, leading to undefined behavior!

How volatile Fixes This

Declaring instance as volatile prevents instruction reordering:

private static volatile Singleton instance;
Enter fullscreen mode Exit fullscreen mode

✔ Ensures that when a thread reads instance, it sees a fully constructed object

✔ Prevents instruction reordering at the JVM or CPU level

✔ Ensures visibility across threads – changes to instance are immediately visible to all


Comparison of Singleton Approaches

Approach Thread-Safe? Lazy Initialization? Performance
Simple Singleton ❌ No ✅ Yes ⚠ Risky in multithreading
Synchronized Method ✅ Yes ✅ Yes 🚫 Slow due to full method lock
Double-Checked Locking ✅ Yes ✅ Yes ⚡ Fast, only locks on first call
Eager Initialization ✅ Yes ❌ No ⚡ Fast but creates instance at startup
Enum Singleton (Best for Java) ✅ Yes ❌ No ⚡ Best approach for Java

Best Practices

  1. Use volatile with double-checked locking to ensure a thread-safe singleton with lazy initialization.
  2. For Java, prefer Enum Singleton if serialization safety is required:
   public enum Singleton {
       INSTANCE;
   }
Enter fullscreen mode Exit fullscreen mode
  1. Avoid full method synchronization (synchronized getInstance()) due to unnecessary performance overhead.
  2. For C++, use Meyers' Singleton, which guarantees thread safety:
   class Singleton {
   public:
       static Singleton& getInstance() {
           static Singleton instance;
           return instance;
       }
   private:
       Singleton() {}
   };
Enter fullscreen mode Exit fullscreen mode

Conclusion

  • The Singleton pattern is widely used in Java but needs proper thread safety.
  • Double-checked locking with volatile is the most efficient way to implement a thread-safe singleton.
  • volatile prevents instruction reordering and ensures visibility across threads.
  • For Java, Enum Singleton is the best choice for simplicity and serialization safety.

Now that you understand how volatile works in a singleton, do you think you’ll implement it in your next project? 🚀 Let me know in the comments! 😊


Further Reading

🔹 Java Memory Model (JMM) and Volatile

🔹 Effective Java by Joshua Bloch (Singleton Pattern)

Hostinger image

Get n8n VPS hosting 3x cheaper than a cloud solution

Get fast, easy, secure n8n VPS hosting from $4.99/mo at Hostinger. Automate any workflow using a pre-installed n8n application and no-code customization.

Start now

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay