Skip to content
Chapter 14 · Core Java

Singleton and Immutable Classes in Java ​

Imagine your application needs to connect to a database. Every time you run a query, you need a connection. Now imagine if your code created a brand new connection every single time you ran a query. That would be catastrophically expensive. Establishing a database connection involves network negotiation, authentication, resource allocation, and more. You absolutely do not want that happening on every insert or select.

What you want is exactly one connection that gets set up once, stays alive, and every part of your application shares that same connection. That is the core problem the Singleton pattern solves.

A Singleton class has one job: guarantee that no matter how many times you ask for an object of that class, from anywhere in your code, you always get back the same exact object. One object, period. This makes it one of the most frequently asked interview topics in Java because there is real depth here. There are six different ways to implement it, and each one exists because the previous approach had a flaw. You need to understand all six.

What Makes a Class a Singleton ​

Before looking at the implementations, understand the two structural ingredients every singleton needs.

First, a private constructor. If the constructor is public, anyone can call new DBConnection() and create a fresh object whenever they want. Making it private locks the door. Nobody outside the class can call new on it.

Second, a public static method to hand out the single instance. Since nobody can call new, you need another way to get the object. A static method works because you can call it on the class itself, without needing an object first. DBConnection.getInstance() gives you the one shared object.

With those two rules in mind, let us walk through all six implementations in order. Each one fixes a real problem from the one before it.

The Six Singleton Implementations ​

Eager Initialization ​

The simplest approach. You create the object right away, as soon as the class is initialized (its first use), even if nobody has asked for the object yet.

java
public class DBConnection {

    // Object created immediately when the class is loaded
    // private: nobody outside this class can touch it
    // static: it belongs to the class, not to any instance
    private static DBConnection con = new DBConnection();

    // Private constructor: nobody can do "new DBConnection()" from outside
    private DBConnection() {
        // Set up the actual database connection here
    }

    // Public static method: the only way to get the object
    public static DBConnection getInstance() {
        return con; // always returns the same object
    }
}

This works. It is thread safe because the JVM guarantees that static variables are initialized safely, once, when the class is initialized. Only one object gets created, and getInstance() always returns it.

The problem is in the name itself. "Eager" means it does the work before you even ask. As soon as the class is initialized, that connection gets created. What if the user starts your application but never actually uses the database feature? The connection was created for nothing. Memory and resources are consumed with no benefit.

That is the one disadvantage: no lazy loading. The object exists whether you need it or not.

Lazy Initialization ​

To fix the eager problem, you delay creating the object until someone actually asks for it.

java
public class DBConnection {

    // Not initialized yet. Just declared. Starts as null.
    private static DBConnection con;

    private DBConnection() {}

    public static DBConnection getInstance() {
        // Only create the object if it does not exist yet
        if (con == null) {
            con = new DBConnection(); // create it on first request
        }
        return con;
    }
}

Now the object is only created when getInstance() is first called. If nobody ever calls it, no connection is ever made. That solves the eager loading waste.

But this introduces a dangerous new problem: threads.

Picture two threads running at the same time, both calling getInstance() for the very first time. Thread 1 checks if (con == null) and sees null. Before it can create the object, Thread 2 also checks if (con == null) and also sees null. Now both threads think they need to create the object. Both execute con = new DBConnection(). Two objects get created. Your singleton is broken.

This race condition is the fatal flaw of lazy initialization.

Synchronized Method ​

The fix for a race condition is synchronization. Lock the method so only one thread can be inside it at a time.

java
public class DBConnection {

    private static DBConnection con;

    private DBConnection() {}

    // synchronized: only one thread can execute this method at a time
    public static synchronized DBConnection getInstance() {
        if (con == null) {
            con = new DBConnection();
        }
        return con;
    }
}

This is thread safe. When Thread 1 enters getInstance(), it acquires the lock. Thread 2 has to wait outside. Thread 1 creates the object, releases the lock, and Thread 2 enters, finds the object already created, and returns it. Problem solved.

But now you have a performance problem. The synchronized keyword puts a lock on the whole method. Every single call to getInstance() has to acquire and release that lock, even after the object has already been created.

Think about what that means in a real application. Suppose you call getInstance() in a hundred different places, many of them running on different threads. After the very first call, the object exists. Every subsequent call just needs to return a reference that already exists. That is trivially fast, right? Except with synchronized on the method, every one of those hundred calls still has to go through the lock and unlock cycle. Locking is expensive. Your code serializes unnecessarily. This makes it very slow.

You only needed the lock once, during creation. After that, the lock is just overhead.

Double Checked Locking ​

The insight: only lock during the first creation. After that, let everyone through freely.

java
public class DBConnection {

    // volatile is mandatory here. We will explain exactly why shortly.
    private static volatile DBConnection con;

    private DBConnection() {}

    public static DBConnection getInstance() {
        // Check 1: fast check without locking
        // After creation, this check passes immediately for everyone
        if (con == null) {

            // Only lock when we might need to create the object
            synchronized (DBConnection.class) {

                // Check 2: verify again inside the lock
                // Another thread might have created it between Check 1 and here
                if (con == null) {
                    con = new DBConnection();
                }
            }
        }
        return con;
    }
}

The logic goes like this. The first if (con == null) check runs without any locking. Once the object is created, this check immediately returns false for all future calls, and they skip the synchronized block entirely. Fast.

The only time a thread enters the synchronized block is when con looks null, meaning the object might not exist yet. Inside the lock, it checks again because another thread might have just created the object between the outer check and acquiring the lock. If it is still null, create it. If not, skip it.

This is why it is called "double checked" locking. You check twice. Once fast without a lock, and once safely inside the lock.

Now, about that volatile keyword. It is not optional. It is absolutely required, and here is why.

Why volatile Is Non Negotiable in Double Checked Locking ​

Without volatile, the Java Memory Model does not promise that a second thread sees what the first thread did, or sees it in the right order. Two problems follow.

Visibility. Thread 1 creates the object and assigns con, but nothing forces Thread 2 to see that new value. Thread 2 can still see null, enter the lock, and (without a proper happens-before link) end up with a second object or a stale value.

Reordering. con = new DBConnection() is really three steps: allocate memory, run the constructor, assign the reference to con. The compiler and CPU may reorder the last two, so con becomes non-null before the constructor has finished. Thread 2's first check sees a non-null con, skips the lock, and uses a half-built object.

volatile fixes both. A write to a volatile variable happens-before every later read of it by any thread, so once another thread sees con as non-null, it is guaranteed to also see everything the constructor did, and the assignment cannot be moved before the constructor.

The remaining downside is a small cost: every read of a volatile variable is slightly more expensive, and the synchronized block still runs once. There is an even cleaner solution.

Bill Pugh Solution (Static Inner Class) ​

This is arguably the most elegant approach. It achieves lazy loading, thread safety, and zero locking with no volatile keyword.

java
public class DBConnection {

    // Private constructor prevents outside instantiation
    private DBConnection() {}

    // This nested class is NOT loaded when DBConnection is loaded.
    // It only gets loaded when someone references it for the first time.
    private static class DBConnectionHelper {
        // The object is created here, with the same guarantees as eager init
        private static final DBConnection INSTANCE = new DBConnection();
    }

    public static DBConnection getInstance() {
        // The first time this line runs, DBConnectionHelper gets loaded,
        // which triggers the creation of INSTANCE.
        // The JVM guarantees this is thread-safe.
        return DBConnectionHelper.INSTANCE;
    }
}

Here is why this works so beautifully. When the JVM initializes the DBConnection class, it does not initialize its nested classes. DBConnectionHelper sits there completely unloaded. No object is created. So you get lazy initialization for free.

The first time someone calls getInstance(), the JVM references DBConnectionHelper.INSTANCE. That reference causes the JVM to load and initialize the DBConnectionHelper class. Class initialization runs the static initializers exactly once, in a thread safe way. This is guaranteed by the Java Language Specification. So the INSTANCE = new DBConnection() line executes once, safely, no matter how many threads call getInstance() simultaneously.

After that first load, DBConnectionHelper.INSTANCE simply returns the already created object. No locks, no volatile, no synchronization needed on subsequent calls. It is fast and clean.

This solution is also called the "static holder" pattern and is widely recommended in professional Java codebases.

Enum Singleton ​

The final approach, and the shortest.

java
public enum DBConnection {
    INSTANCE; // This is the one and only instance

    // Constructor is private by default in enums
    // You cannot write "new DBConnection()" from outside
    
    public void executeQuery(String sql) {
        // run the query
    }
}

// Usage anywhere in your code:
DBConnection.INSTANCE.executeQuery("SELECT * FROM users");

Why is enum the best? Several reasons packed into very few lines.

The JVM guarantees that exactly one instance of each enum constant exists. This is built into the language specification. You get singleton behavior without writing any of the singleton plumbing yourself.

Enum constructors are always private by default. You do not even have to write private. Nobody can call new DBConnection() from outside.

Enums are thread safe by the same JVM class loading guarantees that make eager initialization safe.

In only two lines of code, enum gives you everything that the other five implementations require dozens of lines to achieve.

Immutable Classes ​

Now let us shift to immutable classes, which is a closely related and equally important concept.

An immutable class is a class whose objects cannot have their state changed after construction. You create the object, set its values once, and those values stay fixed forever. The most famous example in Java is String. When you do String s = "hello", that string is immutable. You can create a new string from it, but you cannot change the characters inside it.

Why does immutability matter? Immutable objects are inherently thread safe. If nobody can change the state, there is no way for one thread to corrupt data that another thread is reading. You can share immutable objects freely across threads with zero synchronization. They are also easier to reason about because you always know exactly what they contain.

The Five Rules for Creating an Immutable Class ​

To make a class properly immutable, you follow five rules. Each rule closes a loophole that would otherwise allow someone to change the state.

Rule 1: Declare the class as final.

java
public final class UserProfile {
    // ...
}

If someone can subclass your class, they can override methods and introduce mutable behavior. Making the class final prevents subclassing entirely. No subclass can undermine your immutability guarantee.

Rule 2: Make all fields private and final.

java
public final class UserProfile {
    private final String name;
    private final List<String> petNames;
}

private means nobody outside the class can directly access or change the fields. final means once a value is assigned to the field (in the constructor), it cannot be reassigned. The field will always point to the same value or the same object.

Rule 3: No setter methods.

java
// Do NOT write this:
public void setName(String name) {
    this.name = name; // This would allow mutation. Never do this in an immutable class.
}

Setters exist to change state. An immutable class has no state to change, so it has no setters. Full stop.

Rule 4: Initialize all fields in the constructor.

java
public UserProfile(String name, List<String> petNames) {
    this.name = name;
    this.petNames = new ArrayList<>(petNames); // defensive copy, explained in Rule 5
}

All values come in through the constructor at creation time. After the constructor finishes, no field can ever be changed.

Rule 5: Return defensive copies in getter methods for mutable fields.

This is the most subtle and most commonly misunderstood rule. It deserves a full explanation.

Why Final on a List Does Not Make Its Contents Immutable ​

This trips up a lot of developers. Look at this field:

java
private final List<String> petNames;

The word final here means the petNames variable will always point to the same list object. You cannot reassign petNames to point at a different list. But it says absolutely nothing about the contents of that list. You can still add to it, remove from it, or change elements inside it.

Think of it like a leash attached to a dog. The leash is fixed (final). The leash always connects to this one specific dog. But the dog can still move around, bark, dig holes. The leash being fixed does not constrain what the dog can do.

Here is a concrete example of how this breaks immutability:

java
public final class UserProfile {
    private final String name;
    private final List<String> petNames;

    public UserProfile(String name, List<String> petNames) {
        this.name = name;
        this.petNames = petNames; // MISTAKE: storing the reference directly
    }

    public List<String> getPetNames() {
        return petNames; // MISTAKE: returning the actual list
    }
}

Now watch what happens:

java
List<String> myPets = new ArrayList<>();
myPets.add("Fluffy");
myPets.add("Max");

UserProfile profile = new UserProfile("Alice", myPets);

// The caller still has the original list
myPets.add("Buddy"); // This changes the list INSIDE the UserProfile object!

// Or even worse:
profile.getPetNames().add("Sneaky"); // This also mutates the internal list!

The object is supposed to be immutable, but its internal state just changed. The final keyword on the field did not prevent this because final only locks the reference, not the referenced object's contents.

The Defensive Copy Technique ​

The fix is to copy the data at both entry and exit points.

In the constructor, when you receive a mutable object (like a List or a Date), do not store the reference directly. Make a new copy and store that instead. Now changes to the original list outside the class cannot affect your internal copy.

In the getter, do not return the actual internal field. Return a new copy. That way, even if the caller modifies what they get back, your internal data is untouched.

java
public final class UserProfile {

    private final String name;
    private final List<String> petNames;

    public UserProfile(String name, List<String> petNames) {
        this.name = name;
        // Defensive copy in constructor:
        // Create a new list so the caller's list and our list are separate
        this.petNames = new ArrayList<>(petNames);
    }

    public String getName() {
        // String is already immutable, safe to return directly
        return name;
    }

    public List<String> getPetNames() {
        // Defensive copy in getter:
        // Return a new list so callers cannot modify our internal list
        return new ArrayList<>(petNames);
    }
}

Now see how the behavior changes:

java
List<String> myPets = new ArrayList<>();
myPets.add("Fluffy");
myPets.add("Max");

UserProfile profile = new UserProfile("Alice", myPets);

// Modifying the original list has no effect on the profile
myPets.add("Buddy");
System.out.println(profile.getPetNames()); // prints [Fluffy, Max]

// Modifying what getPetNames() returns has no effect either
List<String> returned = profile.getPetNames();
returned.add("Sneaky");
System.out.println(profile.getPetNames()); // still prints [Fluffy, Max]

Each call to getPetNames() creates a fresh list that is a copy of the internal data. If someone adds to the copy, they are adding to their own copy, not yours. Your original data stays protected.

The caller who adds "Sneaky" to the returned list is holding a reference that nobody else has. When that reference goes out of scope, the garbage collector cleans it up. Your object's internal petNames never changes.

A Complete Immutable Class Example ​

Putting all five rules together:

java
// Rule 1: class is final, no subclassing allowed
public final class UserProfile {

    // Rule 2: all fields are private and final
    private final String name;
    private final int age;
    private final List<String> petNames;

    // Rule 4: all values set once in the constructor
    public UserProfile(String name, int age, List<String> petNames) {
        this.name = name;
        this.age = age;
        // Rule 5 (constructor side): defensive copy of the mutable list
        this.petNames = new ArrayList<>(petNames);
    }

    // Rule 3: only getters, no setters
    public String getName() {
        return name; // String is immutable, safe to return directly
    }

    public int getAge() {
        return age; // primitive, copied by value, safe to return directly
    }

    // Rule 5 (getter side): return a copy, not the actual internal list
    public List<String> getPetNames() {
        return new ArrayList<>(petNames);
    }
}

This object, once created, can never have its internal state changed. It is safe to share across threads. It is predictable. It is honest about what it is.

Check yourself ​

  1. What two ingredients does every Singleton implementation need?
  2. What is the disadvantage of eager initialization? Of simple lazy initialization?
  3. Why is a synchronized getInstance() slow?
  4. In double checked locking, why check con == null twice?
  5. What two problems does volatile solve in double checked locking?
  6. How does the Bill Pugh solution get lazy loading and thread safety without locks?
  7. Why is an enum a Singleton by default?
  8. List the rules for an immutable class. Why does final List not make the list contents immutable?
Answers
  1. A private constructor, and a public static method (such as getInstance()) that returns the single instance.
  2. Eager: the object is created even if it is never used. Lazy: two threads can both see null at the same time and create two objects.
  3. Every call takes and releases the lock, even after the object already exists.
  4. The first check (no lock) makes calls fast once the object exists. The second check (inside the lock) stops a second thread that passed the first check at the same time from creating another object.
  5. Visibility (another thread may not see the new value) and reordering (the reference may be assigned before the constructor finishes). A volatile write happens-before later reads, fixing both.
  6. The instance lives in a private static nested class, which is initialized only when getInstance() first touches it. Class initialization runs once and is thread safe.
  7. Enum constructors are always private and the JVM creates exactly one instance of each constant.
  8. Class final; fields private final; no setters; values set once through the constructor; getters return copies of mutable fields. final only stops the variable from pointing to another list; the list itself can still be changed.