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

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.
In this case:
-
Client1is adding data to the cache. -
Client2is 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.

Let's checkout the code for Singleton Design Pattern:
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:
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:
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.

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:
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:
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
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)
Here's how this works:
-
First check (outside synchronized) avoids locking unnecessarily.
-
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.
The above line is not a single operation. It’s actually:
-
Allocate memory for the object
-
Initialize the object (run constructor)
-
Set connection to point to that memory
Due to JVM optimizations, steps 2 and 3 might be reordered like this:
-
Allocate memory
-
Set connection to point to that memory (⚠️ before it’s initialized!)
-
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:
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.