Double-Checked Locking: Two Checks, Two Different Jobs
Double-checked locking uses two identical if statements that solve different problems. Remove either one and lazy initialization breaks under concurrency.
Double-Checked Locking: Two Seemingly Identical Checks, Different Jobs
Lazily initializing shared state at minimal locking cost means writing two identical-looking ifs. They look redundant, but they solve different problems — remove either one and it breaks.
- Check 1 (no lock): the hot path’s fast exit. If the value is already cached and fresh, return immediately without acquiring any lock → concurrent readers don’t need to queue.
- Check 2 (while holding the lock): covers the gap between “check 1 failed” and “acquiring the lock.” Another thread may have already built the value during that window — checking again avoids a redundant rebuild.
def login(self):
if self._fresh(): # check 1: lock-free fast exit
return self.session
with self._login_lock:
if self._fresh(): # check 2: someone else may have already built it
return self.session
self._do_login() # actually rebuild
return self.session
Which one can be removed?
- Remove #1 → “single-checked locking”: still correct, just acquires the lock on every call.
- Remove #2 → deduplication breaks: two threads that both missed will rebuild and overwrite each other.
When is it actually worth it? Double-checked locking only pays off when the hot read path has real contention and the critical section isn’t cheap. If every call is already dominated by a DB query (~ms), the lock’s cost (~ns) is just noise — at that point DCL is just convention, not a performance win, and a single check is simpler.
Duplicate-looking code isn’t automatically a smell — first ask whether it’s solving different problems.
References:
- https://en.wikipedia.org/wiki/Double-checked_locking
- https://java-design-patterns.com/patterns/double-checked-locking/
Related: see how with lock: guarantees release on exception, or go back to the four-lesson overview on making login thread-safe.