Q29 of 40 · Core Java
How do you write a thread-safe Singleton in Java? Compare 2-3 approaches.
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");