Multithreading fundamentals for LLD
Understand processes, threads, tasks, shared state, and Java concurrency before choosing synchronization tools in low-level design.
Imagine a ticket service with one seat left. Two requests arrive close together. If both requests read “one seat available” before either updates the count, both may confirm a reservation. The code can look correct when requests are handled one at a time and still fail when their work overlaps.
This guide builds the vocabulary and reasoning needed to spot that problem in a Java low-level design (LLD). It starts with processes, threads, and tasks, then uses a small reservation example to explain shared state, race conditions, and the guarantees Java provides. The goal is not to add locks everywhere; it is to identify who owns state, which rule must stay true, and what is the simplest safe way to preserve it.
TL;DR / Mental Model
- A process is a running program with its own address space. Threads are execution paths within a process; they share process resources, including the heap, but each has its own execution state and call stack.
- A task is work to be done. An executor accepts tasks and controls their execution; a thread pool is one common policy that runs tasks on worker threads.
- Concurrency means tasks overlap in time. Parallelism means work is actually running at the same time on multiple execution units.
- Shared mutable state is where many concurrency bugs begin. First identify the invariant, then protect the smallest operation that must behave as one decision.
- Prefer immutable or thread-confined state when it fits. Use an atomic operation for a suitable single-variable update, a lock for a compound invariant, a queue for task handoff, and an executor to manage task execution.
A 30-Second Explanation
Concurrency is about safely coordinating work that can overlap; parallelism is about doing work simultaneously. Threads in one Java process share objects, so two threads can observe and update the same mutable state. To make that safe, identify the business rule that must not be broken, then use the narrowest appropriate guarantee: avoid sharing, use an atomic variable for a simple independent value, or guard a multi-step invariant with the same lock. An executor manages tasks; it does not make the state those tasks share automatically thread-safe.
Process, Thread, and Task
These terms describe different things:
| Term | What it represents | Example |
|---|---|---|
| Process | An isolated running program with its own address space and operating-system resources | A running Java service |
| Thread | An execution path inside a process | A worker handling one request |
| Task | A unit of work that can be scheduled for execution | “Reserve this seat” |
| Executor | A component that accepts tasks and controls when and how they run; some policies use worker threads, others can run on the caller | A configured ExecutorService |
Threads in one process can access the same heap objects. That is useful: threads can work on the same in-memory service or cache without copying the whole process state. It is also the source of risk when multiple threads mutate the same object without coordination. Each thread has its own execution state and call stack; threads do not share one stack.
Thread confinement means only one thread can access and change an object. A local variable is not automatically confined if it refers to an object that is also shared or escapes to another thread.
In Java, Runnable and Callable describe tasks, not threads. A Runnable represents work without a returned result; a Callable<T> can return a result and report an exception. A Thread is one way to run work directly. An ExecutorService accepts tasks and controls their execution; submit can return a Future for observing a result or failure. A thread pool is one common executor policy that runs tasks using managed worker threads, but an executor does not necessarily create a new thread or run work in parallel. In application code, prefer the application's managed executor or a deliberately configured executor over creating a new raw thread for every task. Pool sizing and lifecycle have their own trade-offs, covered in the thread-pool pattern guide.
Java also defines memory-consistency guarantees at task boundaries: actions before submitting a task happen-before its execution begins, and actions taken by a task happen-before a successful return from the matching Future.get(). These guarantees make earlier setup and completed results visible at those boundaries; they do not make shared mutations inside the task automatically thread-safe.
Concurrency Is Not the Same as Parallelism
Two tasks are concurrent when their lifetimes overlap. One task may pause while another runs, even on a machine with a single processor core. The scheduler can switch between them, so their execution overlaps over time without instructions running at the exact same instant.
Two tasks are parallel when they execute at the same instant on different execution units, such as two processor cores. Parallelism can improve throughput for suitable work, but it is not guaranteed just because an application uses threads. Coordination costs, contention, task size, and available hardware all matter.
The distinction is useful in interviews and in production debugging:
- Ask about concurrency when reasoning about correctness: can operations overlap, and what happens if they interleave?
- Ask about parallelism when reasoning about performance: can work use multiple cores, and is the coordination cost worth it?
Adding more threads does not automatically make an application faster. For CPU-heavy work, too many runnable threads can add scheduling overhead. For I/O-heavy work, threads may spend much of their time waiting, so a suitable executor can keep other work moving. The right choice depends on workload and runtime limits.
What a Race Looks Like
Consider a naive reservation method:
boolean tryReserve() {
if (availableSeats > 0) {
availableSeats--;
return true;
}
return false;
}
The check and the decrement are separate operations. With one seat remaining, this interleaving is possible:
| Step | Request A | Request B | Seats |
|---|---|---|---|
| 1 | Checks and reads availableSeats as 1 | 1 | |
| 2 | Checks and reads availableSeats as 1 | 1 | |
| 3 | Reads 1 as the value to decrement | 1 | |
| 4 | Reads 1 as the value to decrement | 1 | |
| 5 | Writes 0 and returns success | 0 | |
| 6 | Writes 0 and returns success | 0 |
Both requests report success for one seat. The exact result depends on the timing of the operations, so this is a race condition: the program's behavior depends on the interleaving.
A data race is a more specific memory-model term: two conflicting accesses to the same variable, at least one a write, are not ordered by a happens-before relationship. Data races can cause visibility and ordering problems as well as surprising updates. In interviews, it helps to name the business-level race condition first (“we confirmed two reservations for one seat”) and then explain the unsynchronized shared access that permits it.
availableSeats-- is not one indivisible operation. It reads the value, calculates a new value, then writes it. Another thread can run between those steps. The same issue appears with count++, if (!map.containsKey(key)) map.put(key, value), and other check-then-act or read-modify-write sequences.
Java Thread States: A Debugging Vocabulary
Java exposes six Thread.State values. They are useful when inspecting a thread dump, but they are not a full trace of operating-system scheduling:
| State | Meaning in Java | Common example |
|---|---|---|
NEW | Created but not started | A new Thread before start() |
RUNNABLE | Executing in the JVM or eligible to run; it can also be waiting for an operating-system resource such as processor time | Running code or ready to run |
BLOCKED | Waiting to acquire an intrinsic monitor lock | Entering a synchronized section held by another thread |
WAITING | Waiting indefinitely for another thread or action | Object.wait() or Thread.join() without a timeout |
TIMED_WAITING | Waiting for a bounded time | Thread.sleep(...) or a timed wait |
TERMINATED | A started thread's execution has completed | Its execution has returned or ended with an uncaught exception |
The names can be easy to misread. RUNNABLE does not prove that the thread is currently executing on a core. BLOCKED specifically means waiting to acquire a Java monitor; it does not mean every form of waiting, I/O, or lock contention. A thread dump is a snapshot, so diagnose the stack trace and the application's synchronization as well as the state label.
Three Guarantees to Think About
When threads share mutable state, ask whether the design needs atomicity, visibility, or ordering. A correct design may need more than one.
- Atomicity: Does this whole operation need to behave as one indivisible decision? A synchronized check-and-decrement can prevent two callers from both claiming the last seat.
- Visibility: After one thread changes a value, what guarantees that another thread can observe the change? Use a defined synchronization mechanism rather than assuming a write becomes visible just because time passed.
- Ordering: What guarantees that one thread's earlier actions are observed before another thread proceeds? Java's happens-before rules establish ordering and visibility between particular actions.
For example, unlocking a monitor happens-before another thread later locks that same monitor. A write to a volatile field happens-before a subsequent read of that field. A happens-before relationship is a guarantee about program actions; it is not a claim that the threads execute simultaneously or that every unrelated variable is automatically protected.
volatile is useful for a shared flag or published reference when its semantics fit. It provides visibility and ordering for that field, but it does not make a compound operation such as count++ atomic, and it does not protect a multi-field invariant. For an independent counter, AtomicInteger provides atomic operations. For a rule involving a check plus an update, or several fields that must change together, use one lock around the whole invariant or redesign the state so a suitable atomic operation can enforce it.
Protect the Invariant, Not Just a Line of Code
The reservation rule is: at most one successful caller may claim the last available seat. Put the check and state change in one critical section on the same inventory object:
final class SeatInventory {
private int availableSeats = 1;
public synchronized boolean tryReserve() {
if (availableSeats == 0) {
return false;
}
availableSeats--;
return true;
}
}
Calls to tryReserve() on the same SeatInventory instance take turns holding that object's monitor. One caller changes the count before the next can check it, so only one succeeds. Synchronizing unrelated instances would not protect this shared seat count. All code that reads or changes the invariant must follow the same locking rule; adding a synchronized method does not protect an unsynchronized path that bypasses it.
Keep a critical section focused on the state transition. Avoid holding a lock while doing network or disk I/O, calling unknown callbacks, or waiting for a slow dependency. Those actions can block other callers and can introduce deadlocks. If a reservation must update several in-memory fields together, keep those updates inside the same critical section; do not assume a thread-safe collection makes a larger business operation atomic.
Choosing a Basic Tool
Start from ownership and the invariant rather than from a favorite concurrency class:
| Situation | Starting point | Why |
|---|---|---|
| A value can be immutable or kept local to one task | Immutability or thread confinement | No coordination is needed if mutable state is not shared |
| One independent counter or flag has a simple atomic update | AtomicInteger, AtomicLong, or AtomicBoolean | The atomic class can provide a single-variable operation |
| A check and update, or multiple fields, must stay consistent | synchronized or an appropriate Lock | One critical section can guard the whole invariant |
| One task hands data to another | BlockingQueue | The queue coordinates handoff and waiting |
| Access to a limited number of resources must be capped | Semaphore | Permits represent the concurrent-access limit |
| Many units of work need managed execution | ExecutorService | It separates task submission from the execution policy; a thread pool is one common policy |
These are starting points, not automatic recipes. For example, ConcurrentHashMap protects its individual operations, but a separate containsKey followed by put is still a compound sequence. Use an atomic compound method such as putIfAbsent when that expresses the intended rule. If the operation must update unrelated objects as one transaction, a concurrent map alone is not enough.
The concurrency interview guide compares Java synchronization tools and works through common design scenarios. For producer/consumer handoff, see the producer-consumer pattern; for mutable ownership and alternatives to sharing, see mutable shared state.
Cancellation and Waiting
Thread coordination also includes how work stops. Java interruption is a cooperative signal, not a command that forcibly terminates arbitrary code. A task should respond to interruption by finishing or cleaning up when appropriate. Methods such as sleep, wait, and join can throw InterruptedException; code should propagate it when possible or restore the interrupt flag if it cannot:
try {
blockingOperation();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
Do not silently swallow an interruption. The caller or executor may be using it to cancel work or shut down. Timeouts and cancellation behavior should be part of the design whenever a task can wait on a lock, queue, or external dependency.
A Practical LLD Reasoning Flow
Thread-safe means that concurrent use preserves the behavior and invariants promised by the object. It does not necessarily mean lock-free or fast. When an interviewer asks whether a class is thread-safe, or a design includes shared state, work through these questions:
- What can overlap? Can two requests or tasks call this object at the same time?
- Who owns the mutable state? Is it local to one operation, confined to one worker, immutable, or shared?
- What must always be true? State the invariant in ordinary language, such as “inventory never becomes negative” or “one id is assigned once.”
- Which actions form one decision? Find the smallest check/update or set of field changes that must be atomic.
- What is the simplest suitable mechanism? Prefer no sharing when practical; otherwise pick a lock, atomic operation, queue, or executor for the actual need.
- What happens under delay, failure, or cancellation? Consider timeouts, interruption, lock ordering, and whether external I/O happens while a lock is held.
- How will you test the invariant? Run competing operations and assert the business outcome, not merely that no exception was thrown.
This gives a concrete explanation: identify the shared state, demonstrate a failing interleaving, name the invariant, then show the mechanism that protects it and its trade-off. The longer concurrency interview guide applies that reasoning to bank transfers, locks, and common LLD questions.
A 5-Minute Explanation
If asked to explain multithreading fundamentals, you can build the answer in this order:
- A process owns an address space; its threads have separate execution stacks but share process objects such as heap data.
- A task is work to execute. Executors separate task submission from the execution policy; a thread pool is one common policy that runs tasks on worker threads.
- Concurrency means work can overlap; parallelism means it actually runs simultaneously. Threads are relevant to correctness even when one core time-slices them.
- Shared mutable state needs an explicit ownership or synchronization rule. A compound action such as checking and decrementing a seat count is not automatically atomic.
- State the invariant, then choose a tool: avoid sharing, use an atomic operation for a suitable single value, or use one lock for a compound invariant. Use queues for handoff and executors to manage tasks.
- Discuss visibility, cancellation, and contention as needed.
volatileis not a general lock, and more threads do not automatically mean more throughput.
Then walk through one race and its fix. This is clearer than listing concurrency classes without explaining which correctness rule each one provides.
Common Mistakes
- Saying “add
synchronized” before identifying the invariant. A lock only helps when it covers the whole compound operation and all relevant callers use the same lock. - Assuming
count++is atomic. It is a read-modify-write sequence; use a fitting atomic operation or protect it with a lock. - Treating
volatileas mutual exclusion. It helps with visibility and ordering for a field; it does not make multi-step logic atomic. - Assuming a concurrent collection makes business logic atomic. Thread-safe individual calls do not automatically combine into one transaction.
- Confusing concurrency with parallelism. Overlap can happen on one core; simultaneous execution requires multiple execution units.
- Assuming every waiting thread is
BLOCKED. That state specifically describes waiting for an intrinsic monitor. - Creating a thread for every task. Thread management, workload limits, and shutdown need a deliberate design; use the application's execution facilities where appropriate.
- Ignoring interruption. Swallowing the signal can prevent cancellation and shutdown from working correctly.
- Expecting threads to guarantee speed. Synchronization, scheduling, and contention all have costs.
Test Your Understanding
Try each question before opening its answer.
Q1. Can two tasks be concurrent on a machine with one processor core? Are they parallel?
Q2. What do threads in the same process share, and what does each thread keep separately?
Q3. Two requests both see one seat available and both return success. What went wrong, and what must the design protect?
Q4. Is volatile int count; count++; a thread-safe counter update?
Q5. Why can ConcurrentHashMap still allow a bug in if (!map.containsKey(key)) map.put(key, value)?
Q6. In Java's Thread.State, what is the distinction between BLOCKED and WAITING?
Q7. Does submitting a task to an executor make objects captured by that task thread-safe?
Q8. A reservation operation updates an available-seat count and records the winning customer id. Is an atomic integer decrement by itself enough?
Q9. A worker catches InterruptedException and ignores it. Why can that cause a shutdown problem?
Further Reading
- Java Language Specification: Threads and Locks — Java's memory model and happens-before rules.
- Java
Thread.StateAPI — the six JVM thread states. - Java concurrency utilities — executors, tasks, futures, synchronization utilities, and memory-consistency guarantees.
Related Articles
- How to handle concurrency scenarios in LLD interviews — a deeper guide to synchronization choices and design scenarios.
- Thread-pool pattern — worker pools, task queues, sizing, and shutdown.
- Producer-consumer pattern — safely handing work between producers and consumers.
- Mutable shared state — reducing or controlling shared mutable data.