Skip to Content
Course content

146: The Singleton Pattern in Java

Click on the "Edit" button in the top corner of the screen to edit your slide content.

In a perfect world, every object in your Java app would have a clear, short lifecycle. But in the real world, you often run into a scenario where having more than one instance of a class is not just redundant, but actually dangerous. Think about a database configuration manager. You don't want five different objects reading the same config.properties file from the disk and holding five identical copies of those settings in memory. You want one source of truth.

Why we can't just use "new"

The core idea of the Singleton pattern is simple: ensure a class has only one instance and provide a global point of access to it. To do this, we have to take away the user's ability to call new MyClass(). I'll do this by making the constructor private.

public class DatabaseConfig {
    private String dbUrl = "jdbc:mysql://localhost:3306/mydb";
    
    // Private constructor prevents other classes from instantiating this
    private DatabaseConfig() {}

    public String getDbUrl() {
        return dbUrl;
    }
}

Now we have a problem: the class is useless because nobody can actually create it. We need a way to hand out the instance. I'll add a static variable to hold the instance and a static method to return it.

Building the basic manager

I'll implement what's called "Lazy Initialization." Instead of creating the instance the moment the app starts, I'll wait until the first time someone actually asks for it. This saves resources if the configuration is never actually needed during a particular execution path.

public class DatabaseConfig {
    private String dbUrl = "jdbc:mysql://localhost:3306/mydb";
    private static DatabaseConfig instance;

    private DatabaseConfig() {}

    public static DatabaseConfig getInstance() {
        if (instance == null) {
            instance = new DatabaseConfig();
        }
        return instance;
    }

    public String getDbUrl() {
        return dbUrl;
    }
}

The race condition I forgot about

Here is where I usually trip up when I'm rushing a prototype. The code above works perfectly in a single-threaded environment. But imagine this is running in a Spring Boot app or a web server with multiple threads. Two threads could call getInstance() at the exact same millisecond. Both see that instance == null is true, and both proceed to call new DatabaseConfig().

Suddenly, you have two "Singletons." This is a classic race condition. I could just add the synchronized keyword to the getInstance() method, but that's a performance killer because every single call to get the config would have to wait in line, even after the instance has already been created.

Making it thread-safe and efficient

To fix this without destroying performance, I'm going to use the "Initialization-on-demand holder" idiom. It uses a private static inner class. Java guarantees that the inner class is not loaded into memory until it is referenced in the getInstance() method, and the JVM handles the thread safety of class loading for us automatically. It's cleaner than double-checked locking and much faster than synchronizing the whole method.

public class DatabaseConfig {
    private String dbUrl = "jdbc:mysql://localhost:3306/mydb";

    private DatabaseConfig() {}

    // This inner class is only loaded when getInstance() is called
    private static class Holder {
        private static final DatabaseConfig INSTANCE = new DatabaseConfig();
    }

    public static DatabaseConfig getInstance() {
        return Holder.INSTANCE;
    }

    public String getDbUrl() {
        return dbUrl;
    }
}

Now, no matter how many threads hit this at once, the JVM ensures only one instance of DatabaseConfig is ever created. If you want the absolute simplest version and don't care about lazy loading, you could also just use an enum, which is the way Joshua Bloch recommends in Effective Java, but the holder pattern is a great way to understand how Java's class loading works to your advantage.




📋 Practical Task

Implementing a Global AppSettings Singleton

You are building a desktop application that needs to track user preferences (like themeColor and fontSize) across multiple different screens. If each screen creates its own settings object, changes made on the 'Settings' screen won't reflect on the 'Main' screen.

Your Task:

  • Create a class named AppSettings.
  • Implement the Singleton pattern using the Static Inner Holder approach discussed in the lesson.
  • Add two private fields: String themeColor (default to "Light") and int fontSize (default to 12).
  • Provide getter and setter methods for both fields.
  • In a separate Main class, instantiate two variables by calling AppSettings.getInstance(). Change the themeColor using the first variable, then print the themeColor of the second variable to verify they are referencing the exact same object.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.