Given a piece of shared state, how do you decide between immutability, thread confinement, and locking, and what are the trade-offs?
answer
- shared mutable state is the enemy; design it away first
- order: confine -> immutable -> lock
- confinement scales best, no locks
- immutable = lock-free + free safe publication, costs allocation
- locking = last resort: contention, deadlock, safe-publication burden
basics
~20 sFirst try to avoid sharing: keep data on one thread (confinement) or make it unchangeable (immutability) so no locks are needed. Only when you must share mutable state do you add locks or concurrent data structures, accepting their cost and complexity.
solid answer
~50 sI treat shared mutable state as the thing to design away first. My default order is: (1) Confinement, keep the data on a single thread (stack-confined locals, per-thread/actor ownership, ThreadLocal) so there's nothing to coordinate, which scales best and needs no locks. (2) Immutability, model the data as immutable value objects; they're inherently thread-safe, freely shareable, safe to publish even through a data race, and great as cache/map keys, at the cost of allocation when 'changing' them. (3) Only when state is genuinely shared and mutable do I add synchronization, lock-guarded fields, concurrent collections, or atomics, accepting contention, deadlock/livelock risk, and the discipline of correct safe publication. The right choice depends on read/write ratio, object size and churn, latency vs throughput needs, and how often the data crosses thread boundaries. Mostly-read, moderate-size data favors immutability; high-churn single-owner data favors confinement; truly shared read-write data needs the right concurrent abstraction.
go deeper
Knows the three strategies exist and that immutable/confined data avoids locks; may not reason about trade-offs yet.
Can pick a reasonable strategy per case and justify it (read/write ratio, sharing), and knows concurrent collections vs locks.
Articulates the confine->immutable->lock preference order with concrete trade-offs (allocation cost, contention, deadlock, safe publication) and chooses the narrowest correct tool.
Designs system-wide state ownership: partitions state to confine it, defines immutable message/value contracts across boundaries, minimizes the locked surface, and sets conventions; weighs latency/throughput/operability and the maintainability cost of lock-based code.
## The governing principle Nearly every Java concurrency hazard comes from **shared mutable state** — data that more than one thread can both see and change. The senior insight is that locking is the *last* resort, not the first: you get simpler, more scalable systems by **eliminating** sharing or **eliminating** mutability before reaching for locks. ## Option A: Confinement (eliminate sharing) Keep each piece of data owned by exactly one thread. - **Forms:** stack confinement (locals that don't escape), ad-hoc/per-thread ownership (single-writer principle, actor-style mailboxes, event-loop designs), and `ThreadLocal` for per-thread copies. - **Pros:** zero coordination, no locks, perfect scalability (no contention), simplest reasoning. - **Cons:** not always possible (some data is inherently shared); enforced only by discipline except for stack-confined primitives; `ThreadLocal` can leak in thread pools; cross-thread communication must be handled explicitly (e.g. message passing). - **Best when:** the data has a natural single owner, work can be partitioned per thread/request, or churn is high. ## Option B: Immutability (eliminate mutability) Model the data as immutable value objects. - **Pros:** inherently thread-safe; shareable with no locks; **free safe publication** (all-final fields are visible even through a data race); usable as `HashMap` keys and cache entries because identity/`hashCode` are stable; trivially reasoned about and tested. - **Cons:** every logical change allocates a new object, which costs CPU and GC pressure; large objects or hot mutation loops can become expensive; you sometimes need structural sharing (persistent data structures) to keep it cheap. - **Best when:** data is read far more than written, is small/medium, crosses threads often, or serves as configuration/event payloads/keys. ## Option C: Synchronization (guard the shared mutable state) When sharing *and* mutation are both unavoidable. - **Tools (in rough order of preference):** concurrent collections (`ConcurrentHashMap`, `CopyOnWriteArrayList`, `BlockingQueue`), atomics (`AtomicInteger`, `AtomicReference`, `LongAdder`), `volatile` for simple visibility flags, then explicit locks (`synchronized`, `ReentrantLock`, `ReadWriteLock`/`StampedLock`). - **Pros:** the only correct option for genuinely shared read-write state; mature, well-understood tools. - **Cons:** contention serializes threads and can dominate latency; risk of deadlock, livelock, lock convoying, and priority inversion; you must reason about lock scope, ordering, and **safe publication** of anything you hand out; harder to test and to get right. - **Best when:** the state is intrinsically shared and frequently mutated by multiple threads (counters, caches, queues, connection pools). ## A decision checklist 1. **Can this data live on one thread?** -> Confine it. (Cheapest, most scalable.) 2. **If it must be shared, can it be immutable?** -> Make it a value object. (Safe and lock-free.) 3. **If it must be shared *and* mutable:** pick the narrowest concurrent abstraction (concurrent collection > atomic > volatile flag > explicit lock), keep critical sections short, define a lock-ordering policy, and ensure safe publication of escaping references. ## Trade-off axes to reason about - **Read/write ratio:** read-heavy -> immutability or `CopyOnWriteArrayList`/snapshots; write-heavy -> confinement or fine-grained locks/atomics. - **Object size & churn:** big or rapidly-changing -> immutability's allocation cost hurts; favor confinement or in-place mutation under a lock. - **Latency vs throughput:** locks add tail latency under contention; lock-free/immutable designs give more predictable latency. - **Crossing thread boundaries:** the more often data is handed between threads, the more immutability (free safe publication) pays off. - **Operability:** confinement and immutability are far easier to test, reason about, and debug than lock-based code; that maintainability is itself a first-class trade-off. ## Combining the strategies Real systems mix all three: confine request-scoped state per worker thread, pass **immutable** messages between threads, and use a few **concurrent** data structures at the shared seams (queues, caches). The art is pushing as much state as possible into the confined/immutable categories so the locked surface stays small.
- When would you NOT choose immutability despite its safety benefits?When the object is large or mutates very frequently in a hot path: each change allocates a new object, creating CPU and GC pressure. There, in-place mutation under a lock, an atomic, or confinement to a single owner is usually cheaper. Persistent (structurally shared) data structures can mitigate this.
- Is using a ConcurrentHashMap enough to make multi-step updates thread-safe?No. Individual operations are atomic, but compound check-then-act sequences (e.g. get-then-put) are not. You must use atomic methods like compute/merge/putIfAbsent or external synchronization for the compound operation.
saying these in an interview costs you the question
- Reaching for synchronized first instead of trying to remove sharing or mutability
- Claiming immutability is always best while ignoring allocation/GC cost for big or high-churn objects
- Forgetting that escaping references from a locked structure still need safe publication
- Treating ThreadLocal as free without acknowledging pool leaks
- Assuming concurrent collections make compound check-then-act operations atomic