What is lazy initialization for a singleton, and why might a naive synchronized getter become a performance concern under high contention?
answer
- Lazy = build on first use, not at startup
- synchronized getter = correct but locks every call
- Lock serializes threads even after instance exists
- Fast-path cost is the motivation for DCL
basics
~20 sLazy initialization means you only create an object the first time it is actually needed, not at startup. Putting synchronized on the whole getter makes it thread-safe but forces every caller to wait for the lock even after the object already exists.
solid answer
~40 sLazy initialization defers creating a singleton until its first use, saving startup cost or resources when the object may never be needed. The simplest thread-safe version marks the entire accessor `synchronized` so only one thread runs the create-if-null check at a time. The correctness is fine, but the cost is that the lock is acquired on every single call, including the millions of calls after the instance already exists and no creation happens. Lock acquisition has overhead and serializes threads, so under high contention this getter becomes a bottleneck. Double-checked locking was invented to avoid paying that synchronization cost on the common already-initialized path while still being safe on the first call.
go deeper
Can explain lazy = create on first use, and that a synchronized method is the simple safe version.
Articulates the race condition in the unsynchronized getter and the per-call lock cost of the synchronized one.
Frames the synchronized getter's cost (lock acquisition + thread serialization on the hot path) as the precise motivation for DCL and the holder idiom.
Weighs lazy vs eager as an architectural trade-off (startup time, resource lifetime, testability) and knows when the simplest synchronized version is the right answer because contention is negligible.
## The problem lazy init solves A **singleton** is an object you want exactly one instance of in a program (for example a configuration cache or a connection pool). **Eager initialization** creates it when the class loads, whether or not it is ever used. **Lazy initialization** instead creates it the first time someone asks for it — useful when the object is expensive to build, or might never be needed. The classic lazy getter looks like: ```java private static Config instance; public static Config getInstance() { if (instance == null) { // (1) check instance = new Config(); // (2) create } return instance; } ``` ## Why this is broken with multiple threads A **thread** is an independent line of execution; multiple threads can run this method at the same time. Suppose thread A runs the `instance == null` check and sees `null`, then the operating system pauses it before line (2). Thread B now runs, also sees `null`, and creates an instance. Thread A resumes and creates a *second* instance. Now you have two singletons — a **race condition** (a bug whose outcome depends on the unpredictable timing of threads). ## The synchronized fix and its cost The `synchronized` keyword forces threads to take turns: only one thread at a time may execute a synchronized block, by acquiring a **lock** (also called a monitor) on an object. Marking the whole getter synchronized fixes the race: ```java public static synchronized Config getInstance() { if (instance == null) { instance = new Config(); } return instance; } ``` Now only one thread can run the check-and-create at a time, so exactly one instance is built. This is **correct**. The downside is *performance*: acquiring a lock costs CPU cycles and, more importantly, **serializes** threads — if 50 threads call `getInstance()`, they queue up one behind another even though, after the very first call, the instance already exists and nobody needs to create anything. You pay the lock cost forever for a creation that happens once. ## What comes next The desire to skip locking on the already-initialized 'fast path' is exactly what motivates the **double-checked locking** pattern (check without a lock first, lock only if it looks uninitialized). That pattern is famously broken in Java without the `volatile` keyword — which is the heart of this topic — but understanding *why* people reached for it starts here: the synchronized getter is correct but slow on every call.
- If lazy init is risky, why not always initialize eagerly?Eager init builds the object at class-load time even if it is never used, paying its cost and holding its resources unnecessarily; lazy init is worth the complexity when the object is expensive or optional, or when its construction depends on state not available at class load.
- Does the synchronized getter ever give a wrong instance?No — it is fully correct and always returns one consistent instance. Its only drawback is the lock cost paid on every call, which is what double-checked locking tries to eliminate.
saying these in an interview costs you the question
- Saying a plain `if (instance == null)` getter is thread-safe (it has a race condition)
- Claiming synchronized is incorrect — it is correct, just slow on the hot path
- Confusing lazy init with eager static initialization