NailYourInterview
Creational Design Patterns

Singleton Design Pattern in Java with Multhithreaded Code

Learn the Singleton Design Pattern with a real Java example. Understand how to safely implement it in multithreaded apps with lazy and eager initialization.

Singleton Design Pattern

Video thumbnail

The Singleton Pattern is a creational design pattern that ensures a class has only one instance, and it provides a way to access that instance globally.

You might wonder: "Why would I ever want to use an object globally?"

Well, in some cases, that’s exactly what we need especially when the object is shared across the entire application, like a cache, a logger, or a configuration manager. Let's understand the problem by creating multiple instances where we shouldn't.

Problems When We Create Multiple Instances

Inconsistent State for Shared Resources

Let’s say we want to build a common in-memory cache, where different parts of our application can store and retrieve data.

public class CacheManager {
    private Map<String, String> cache = new HashMap<>();
 
    public void addToCache(String key, String value) {
        cache.put(key, value);
    }
 
    public String getFromCache(String key) {
        return cache.get(key);
    }
}

In this case:

  • Client1 is adding data to the cache.

  • Client2 is trying to retrieve it — but fails.

That’s because both clients are using separate instances of CacheManager. So the cache data isn’t being shared and that defeats the whole purpose of having a common cache.

This is why we need a Singleton. When resources like caches, thread pools, loggers, or configuration managers are meant to be shared, using multiple instances leads to bugs and confusion.

Higher Object Creation Cost

Some objects are expensive to create. For example:

  • Reading from disk (e.g., loading a config file)

  • Opening a network connection

  • Initializing large in-memory data structures

Creating such objects again and again in different parts of your app slows things down and wastes resources.

Wouldn’t it be better to create them just once, and reuse them everywhere? That’s exactly what Singleton helps with.

Unnecessary Memory Usage

Every time you create a new object, it takes up memory.

Let’s say 5 different parts of your app each create their own instance of a logger or a configuration manager even though they’re all doing the same thing.

You’re now:

  • Using 5x memory

  • Doing the same setup 5x

  • Making your app harder to manage

With Singleton, you avoid this waste by having just one shared instance.

How to Create a Singleton Object?

Now that we understand why and when we should use the Singleton pattern, let’s see how to implement it.

We’ll take a simple and realistic example — a database connection.

In most applications, opening a database connection is expensive. So instead of creating a new connection every time, we want to reuse the same connection throughout the app.

Achieving Singleton Design Pattern
With Singleton Design Pattern

Let's checkout the code for Singleton Design Pattern:

public class DBConnection {
 
    private static DBConnection connection;
 
    private DBConnection(String name) {
        System.out.println("Connection to DB is established by " + name);
    }
 
    public static DBConnection getDBConnection(String name) {
        if (connection == null) {
            connection = new DBConnection(name);
        }
        return connection;
    }
}

Common Questions

The constructor is private so how is the DBConnection object created?

Good catch! Since the constructor is private, no one outside the class can directly create an object using new DBConnection(...).

But we’re providing a static method getDBConnection() that belongs to the class itself. So, instead of using new, we call: DBConnection.getDBConnection(...)

This method handles the object creation inside the class, and returns it to us.

What if I call getDBConnection() multiple times? Will it create multiple objects?

Nope! That’s the beauty of Singleton.

Take a look at this condition:

if (connection == null) {
    connection = new DBConnection(name);
}

This ensures that the object is created only once. After that, the same object is reused no matter how many times you call getDBConnection().

Got it! But how do I actually use or access this object?

It’s super simple. Since the method getDBConnection() returns the object, you can just use it like this:

DBConnection db1 = DBConnection.getDBConnection("Client1");
DBConnection db2 = DBConnection.getDBConnection("Client2");

Output: Connection to DB is established by Client1

Even though you're calling the method twice with different names, only the first call creates the object. That’s why "Connection to DB is established by Client1" is printed only once.

The second call simply returns the already created instance so "Client2" has no effect on object creation or output.

This is how Singleton ensures a single shared instance is used across the entire application.

UML Diagram for Singleton Design Pattern
UML Diagram for Singleton Design Pattern

Problem with Current Singleton in Multithreaded Environment

Our current Singleton works perfectly in single-threaded applications.

But in a multithreaded environment, things can go wrong. Imagine two threads trying to create the object at the same time. Both threads might enter this block:

if (connection == null) {
    connection = new DBConnection(name);
}

Now both threads think connection is null, and both try to create the object. This breaks the Singleton rule because now we have two instances.

Let’s simulate it with a delay:

public class DBConnection {
    private static DBConnection connection;
 
    private DBConnection(String name) {
        System.out.println("Connection to DB is established by " + name);
    }
 
    public static DBConnection getDBConnection(String name) {
        if (connection == null) {
            // Simulate delay to increase the chance of thread overlap
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            connection = new DBConnection(name);
        }
        return connection;
    }
}

Output might be:

Connection to DB is established by Thread1
Connection to DB is established by Thread2

That's not a Singleton anymore!

Fixing the Problem

Option 1: Using synchronized

DBConnection.java
public class DBConnection {
    private static DBConnection connection;
 
    private DBConnection(String name) {
        System.out.println("Connection to DB is established by " + name);
    }
 
    public static synchronized DBConnection getDBConnection(String name) {
        if (connection == null) {
            connection = new DBConnection(name);
        }
        return connection;
    }
}

Output: Connection to DB is established by Thread1
Or maybe Thread2 — whichever thread reaches first. But only one line will be printed.

This works, but it synchronizes the method every time, even when the object is already created. That can slow things down.

Option 2: Double-Checked Locking (Better Performance)

DBConnection.java
public class DBConnection {
    private static volatile DBConnection connection;
 
    private DBConnection(String name) {
        System.out.println("Connection to DB is established by " + name);
    }
 
    public static DBConnection getDBConnection(String name) {
        if (connection == null) { // First check (no locking)
            synchronized (DBConnection.class) {
                if (connection == null) { // Second check (with lock)
                    connection = new DBConnection(name);
                }
            }
        }
        return connection;
    }
}

Here's how this works:

  1. First check (outside synchronized) avoids locking unnecessarily.

  2. Second check (inside synchronized) ensures only one thread creates the object.

Here, we have locked only the small block of function in DBConnection Class Object.

Why We Need volatile

You may wonder: what’s the need for volatile?

Thread Caching Problem

Threads often cache the variables. So, Thread A might create the object, but Thread B could still see connection as null because it hasn’t updated its cache.

Due to compiler and CPU optimizations, the JVM might reorder instructions for performance.

connection = new DBConnection(name);

The above line is not a single operation. It’s actually:

  1. Allocate memory for the object

  2. Initialize the object (run constructor)

  3. Set connection to point to that memory

Due to JVM optimizations, steps 2 and 3 might be reordered like this:

  1. Allocate memory

  2. Set connection to point to that memory (⚠️ before it’s initialized!)

  3. Run the constructor

So now:

  • Thread A currently on step 3 is still initializing the object (running constructor)

  • Thread B at the same time comes in and says: “Oh cool! connection is not null (since step 2 pointed connection to memory) — I’ll use it now!”

    But it’s using an object that hasn’t finished being constructed yet. That’s what we mean by a "half-constructed object". The reference exists, but the object internals are still being built.

Declaring connection as volatile prevents caching and reordering. It ensures every thread sees the most up-to-date and fully initialized object.

In interviews, you might be asked about Eager vs Lazy Initialization.

  • Lazy Initialization: The object is created only when needed (like our example).

  • Eager Initialization: The object is created when the class is loaded. This is thread-safe by default but may waste memory if the object is never used.

Example for Eager Initialization:

private static final DBConnection connection = new DBConnection("Connection 1");

The object connection is created at the time the class is loaded into memory, not when it is first used. This happens because the variable is marked static and initialized directly.