Overview
Thread safety means an object behaves correctly when accessed by multiple threads at once. Without it, race conditions occur: two threads read a counter, both increment, and one update is lost; two gates assign the same parking spot; two users book the same seat. Concurrency questions are a frequent part of LLD interviews for parking lots, booking systems, rate limiters, caches, and schedulers.
Tools include immutability (no shared mutable state), confinement (one thread owns the data), atomic variables (AtomicInteger, compare-and-set), locks (synchronized, ReentrantLock, read-write locks), concurrent collections (ConcurrentHashMap, BlockingQueue), and higher-level designs like single-writer queues and actors. The key skill is identifying check-then-act and read-modify-write sequences that must be atomic.
Both cashiers check the system, see one ticket left, and sell it at the same moment. A lock is a rule that only one cashier can touch the ticket drawer at a time, so the second sees 'sold out'.
When to use it
- Shared state accessed by multiple threads or requests.
- Allocating limited resources (spots, seats, tokens).
- Background workers and schedulers.
Where it shows up in interviews
Recognize it when: multiple gates or users claim limited items.
- Design a parking lot with multiple entrances
- Design a movie ticket system
- Design an elevator system
Recognize it when: concurrent reads and writes.
- Design a thread-safe LRU cache
- Design a rate limiter
- Design a task scheduler
Where it is used in real software
ConcurrentHashMap, AtomicLong, ReentrantLock, and ExecutorService are the standard toolkit.
SELECT ... FOR UPDATE and optimistic version checks serialize access across servers.
Single-threaded JavaScript avoids data races in memory but still faces races across async awaits and processes.
Key terms
- Race condition
- Outcome depends on thread timing.
- Critical section
- Code that must run exclusively.
- Atomicity
- Operation happens entirely or not at all, with no interleaving.
- Visibility
- Writes by one thread seen by others (volatile, happens-before).
- Deadlock
- Threads wait on each other's locks forever.
How it works, step by step
- 1Identify shared mutable state
Fields accessed by multiple threads.
- 2Find compound actions
check-then-act, read-modify-write.
- 3Choose a strategy
Immutability, atomics, locks, concurrent collections, or confinement.
- 4Keep critical sections small
No I/O inside locks.
- 5Avoid deadlocks
Consistent lock ordering, timeouts; test with many threads.
Race on the last parking spot
Two gates call park() at the same moment; one spot left
| Time | Gate A | Gate B | Result |
|---|---|---|---|
| t1 | findFreeSpot() -> S7 | - | - |
| t2 | - | findFreeSpot() -> S7 | - |
| t3 | assign(S7, car1) | - | - |
| t4 | - | assign(S7, car2) | Two cars, one spot |
| Fix | Atomic claim (lock or compareAndSet) | Second claim fails, tries next spot | Correct |
NOWTime: t1 | Gate A: findFreeSpot() -> S7 | Gate B: - | Result: -
The check (find free spot) and act (assign) must be one atomic operation.
Implementation
public final class ParkingSpot { private final String id; private final AtomicReference<String> occupant = new AtomicReference<>(); public ParkingSpot(String id) { this.id = id; } // Atomic claim: only one thread can move from null to a plate public boolean tryOccupy(String plate) { return occupant.compareAndSet(null, plate); } public void release() { occupant.set(null); } public boolean isFree() { return occupant.get() == null; } public String id() { return id; }} public final class Floor { private final List<ParkingSpot> spots; public Floor(List<ParkingSpot> spots) { this.spots = List.copyOf(spots); } public Optional<ParkingSpot> park(String plate) { for (ParkingSpot s : spots) { if (s.isFree() && s.tryOccupy(plate)) return Optional.of(s); // retry next spot if lost race } return Optional.empty(); }} // Thread-safe counter per key without explicit locksConcurrentHashMap<String, LongAdder> hits = new ConcurrentHashMap<>();hits.computeIfAbsent("GET /orders", k -> new LongAdder()).increment();Complexity and performance
May retry under contention.
Keep critical sections tiny.
Trade-offs
One big lock is simple but limits parallelism; per-item locks scale but risk deadlocks.
Locks block; optimistic CAS or version checks retry and suit low contention.
Variants and related techniques
One thread owns mutations; others send messages (actors, event loops).
Many concurrent readers, exclusive writers.
Across processes: database locks, Redis, etcd.
Common mistakes
- Check-then-act without atomicity.
Fix: Combine into one atomic operation (CAS, putIfAbsent, lock).
- I/O inside synchronized blocks.
Fix: Hold locks only for in-memory updates.
- Inconsistent lock ordering.
Fix: Always acquire locks in a global order to avoid deadlocks.
Interview questions
How do you prevent two gates from assigning the same spot?
Make the claim atomic: compareAndSet on the spot's occupant, a lock per floor or spot, or a single allocator thread consuming requests from a queue. The losing gate retries with the next free spot.
What is the difference between synchronized and AtomicInteger?
synchronized locks a block of code for mutual exclusion and visibility; AtomicInteger uses lock-free compare-and-set for single-variable updates, which is faster under low contention but cannot protect multi-variable invariants.
Practice problems
| Problem | Difficulty | What it trains |
|---|---|---|
| Thread-safe LRU cache | Medium | Locks around linked map. |
| Concurrent seat booking with holds | Hard | Atomic claims and expiry. |