Q29 of 40 · Core Java

How do you write a thread-safe Singleton in Java? Compare 2-3 approaches.

Core JavaMidsingletondesign-patternsconcurrencythread-safetyjvm

Short answer

Short answer: The cleanest modern approach is the initialization-on-demand holder (IODH) idiom or an enum singleton. Double-checked locking with volatile works but is tricky. Eager initialisation via a static field is correct and simple if instantiation cost is acceptable at class load time.

Detail

1. Eager initialisation (static field): The simplest correct approach. The JVM guarantees class initialisation is thread-safe, so private static final Instance INSTANCE = new Instance() creates the singleton exactly once. The downside: the instance is created even if never used. Acceptable for most infrastructure objects.

2. Initialization-on-demand holder (IODH) idiom: Lazy with zero synchronisation overhead. A nested static class Holder holds the instance. The JVM defers Holder's class initialisation until its fields are first accessed. Because the JVM guarantees class loading is synchronised, the singleton is created exactly once, lazily, with no synchronized keyword anywhere.

3. Double-checked locking (DCL) with volatile: The classic attempt that was broken before Java 5. Without volatile, the JIT could reorder the write and make the reference visible before the object was fully constructed. With volatile, it is now correct but adds synchronisation overhead on every read (though the OS read barrier is cheap). Harder to read than IODH for no practical gain.

4. Enum singleton (Effective Java recommendation): Thread-safe, lazy-ish (initialised on first class access), and immune to serialisation creating a second instance. Concise. The limitation: if you need to extend a class, enums can't extend (only implement interfaces).

For test automation, singletons appear in shared driver managers, configuration holders, and shared API clients. The enum or IODH approach is preferred for new code; DCL appears in legacy codebases and should be recognised.

// EXAMPLE

SingletonPatterns.java

// 1. Eager initialisation — simple and correct
public class ConfigHolder {
    private static final ConfigHolder INSTANCE = new ConfigHolder(); // JVM-safe
    private ConfigHolder() { loadConfig(); }
    public static ConfigHolder getInstance() { return INSTANCE; }
}

// 2. Initialization-on-demand holder — lazy, zero sync overhead
public class DriverManager {
    private DriverManager() { /* init driver */ }

    private static final class Holder {
        static final DriverManager INSTANCE = new DriverManager();
        // JVM class loading is synchronised — created exactly once, lazily
    }

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

// 3. Double-checked locking — correct with volatile, but prefer IODH
public class TokenCache {
    private static volatile TokenCache instance; // volatile required!

    private TokenCache() {}

    public static TokenCache getInstance() {
        if (instance == null) {                  // first check (no lock)
            synchronized (TokenCache.class) {
                if (instance == null) {          // second check (under lock)
                    instance = new TokenCache();
                }
            }
        }
        return instance;
    }
}

// 4. Enum singleton — Effective Java recommendation
public enum ApiClient {
    INSTANCE;
    private final HttpClient http = HttpClient.newHttpClient();
    public HttpResponse<String> get(String url) throws Exception {
        return http.send(HttpRequest.newBuilder(URI.create(url)).build(),
            HttpResponse.BodyHandlers.ofString());
    }
}
ApiClient.INSTANCE.get("https://api.example.com/users");

// WHAT INTERVIEWERS LOOK FOR

Why eager initialisation is safe (JVM class loading guarantee), why DCL was broken before Java 5 and needs volatile, the IODH idiom as the cleanest lazy option, and the enum approach. Strong answers explain WHY volatile is needed in DCL (constructor visibility / publication safety).

// COMMON PITFALL

DCL without volatile. Before Java 5's JMM changes, and even today without volatile, the JIT can publish the reference before the constructor completes — other threads see a partially-constructed object. This is one of the most dangerous JMM pitfalls and a frequent senior interview question.