The simplest fix - locking every getInstance() call - is correct but destroys performance. You pay the lock cost millions of times when you only need it once.
The Obvious Solution
Wrap the entire method in a lock. Only one thread can enter at a time. Problem solved, right? Technically yes. Practically no.
The Performance Problem
The instance only needs to be created once. After that first call, the instance exists forever. But with naive locking, every single call to getInstance() acquires and releases a lock. That is millions of unnecessary lock operations.
Lock Overhead: First Call vs Millionth Call
The Numbers
Lock acquisition: 10-100 nanoseconds on modern hardware. Sounds tiny. But call getInstance() in a hot loop? 10 million calls per second is not unusual. That is 100ms-1s of pure lock overhead per second.
Contention makes it worse: When multiple threads hit the lock simultaneously, only one proceeds. Others wait. Under high concurrency, threads spend more time waiting than working.
What We Really Want
Lock only during creation. Once the instance exists, skip the lock entirely. Read a pointer - that is it. The challenge is doing this safely.
The Trade-off: Correctness is easy with full locking. Performance is easy without locking. Getting both requires a clever trick - and that trick has its own pitfalls.
💡 Key Takeaways
✓Naive locking wraps the entire getInstance() in a synchronized block. Correct but slow - every call pays the lock cost.
✓Lock acquisition costs 10-100 nanoseconds. At millions of calls per second, this becomes significant overhead.
✓After the first call, the instance exists. Every subsequent lock acquisition is pure waste - we are just reading a pointer.
✓Contention multiplies the problem. Under high concurrency, threads queue up waiting for a lock they do not actually need.
✓The goal: lock only during creation (once), then never lock again. This is where double-checked locking comes in.
📌 Examples
1Web server calling getInstance() for every request. 10,000 requests/second means 10,000 unnecessary lock operations per second.
2Logging framework where every log statement calls getInstance(). In a verbose application, this is millions of calls per minute.
3Service locator pattern where getInstance() is called to retrieve any service. Every dependency injection becomes a lock acquisition.