LearnThatStack Ace your next interview

Concurrency & Multithreading

Examples in Java unless a row names a language Checked Oct 2026 23 code snippets

The model

  • Threads in one process share its memory. Most concurrency bugs start with shared mutable state, so find it first and decide what guards it.
  • A race is a bug whose result depends on timing. count++ reads, adds and writes in three steps, so two threads can both read 5 and both write 6.
  • A mutex makes threads take turns through a critical section. Keep that section short. Take several locks in one fixed order, or threads can deadlock.
  • Without a lock, an atomic or a memory barrier, a thread may never see another thread's write. That's a visibility bug, and it differs from a race.
  • Concurrency overlaps tasks that wait. Parallelism runs tasks on several cores at once. Async I/O helps work that waits. Only more cores speed up work that computes.
Thread Aruns count++Shared countand its mutexThread Bruns count++NO LOCKread 5read 5write 6write 6 A's +1 is lostMUTEXlock, read 5lock waits for Awrite 6, unlockread 6, write 7
Each arrow is one step, top to bottom. With no lock, A and B both read 5 and write 6, so one update is lost. With a mutex, B waits, then reads 6 and writes 7.

Races & shared state

why threads go wrong, and what makes code safe

Concurrency or parallelism

Tell concurrency from parallelismConcurrency overlaps tasks in time, even on one core. Parallelism runs them at once on several cores.
Pick the one a job needsConcurrency for work that waits, parallelism for work that computes.

TrapMixing them up adds cores to work that only waits, or threads to work that never waits.

Data race or race condition

Define a data raceTwo threads access the same memory without synchronization, and at least one writes.
Have a race condition with no data raceLock every access but assume A arrives before B. The order can still be wrong.

TipA mutex or atomic fixes a data race. A logic race needs a new protocol, not more locks.

TrapIn C++, any data race is undefined behavior, even one whose output looks right.

Race conditions

Define a race conditionA bug where the result depends on thread timing.
Explain how an increment loses updatescount++It reads, adds and writes in three steps. Both threads read 5, and both write 6.
Spot a check-then-act raceif (!map.has(k)) map.set(k, v)Both pass the check, then both write. In JavaScript, an await between them is enough.
Say why a race is hard to reproduceThe bad timing is rare. Production load and GC pauses widen the window.

TrapRerunning until it fails doesn't debug a race. Find the shared state and guard it.

Make code thread-safe

Define thread-safeIt works under any interleaving, with no extra locking by callers.
Make shared data safeMake it immutable, keep it in one thread, or guard it with a lock or atomic.
Give each thread its own copyUse a ThreadLocal. In a pool, call remove() in finally, or the next task inherits the value.

TrapTwo thread-safe calls in a row can still race, like a check then an insert. One lock must cover both.

Locks & critical sections

let one thread at a time touch shared state

Mutexes and critical sections

Stop a race with a mutexLock before touching shared data, unlock after. Other threads wait their turn.
Define a critical sectionThe code that touches shared state. A lock lets one thread run it at a time.
Release the lock even when the code throwslock.lock(); try { work(); } finally { lock.unlock(); }
Keep a critical section shortCopy what you need, release the lock, then do the slow work.

TrapNever hold a lock across I/O, a network call or a long loop. Every thread that needs it waits.

Reentrant locks

Take a lock you already holdUse a reentrant lock. It counts the owner's holds and frees the lock at zero.
Say what a plain mutex does thenIt deadlocks. The thread waits on itself forever.

TrapThe inner call may see half-updated data from the outer one. Take the lock once, in the public method.

Read-write locks

Let many readers in at onceUse a read-write lock. Readers share it, and a writer holds it alone.
Say when it beats a mutexWhen reads far outnumber writes and each read holds it a while. Otherwise a mutex wins.

TrapA steady stream of readers can starve a writer, unless the lock prefers writers.

Spin or block

Wait for a lock that may take a whileBlock. The OS parks the thread until the lock frees.
Wait for a lock held under a microsecondSpin, which skips a context switch. Real locks spin briefly, then block.

TrapSpinning on one core, or under heavy contention, starves the lock holder of CPU.

Deadlock & starvation

when threads stop making progress

Deadlock and lock ordering

T1lock BT2lock A
two ordersT1 holds A and wants B. T2 holds B and wants A. The arrows close a cycle.
T1lock BT2lock A
one orderBoth take A first. T2 holds nothing while it waits, so T1 gets B and finishes.
Define a deadlockThreads each wait for a lock another holds. They hang, and nothing crashes.
Name the four conditionsMutual exclusion, hold and wait, no preemption, circular wait. Break any one.
Prevent deadlockTake locks in one global order, say by ID. No cycle can form.
Back off when one order can't worklock.tryLock(50, TimeUnit.MILLISECONDS)On a timeout, release what you hold and retry.
Find a deadlock in a running JVMjstack <pid>It dumps every thread and reports any lock cycle.
Recover a deadlocked serviceAbort a victim, as databases do, or drain and restart.

TrapLock ordering only works if every code path obeys it.

Livelock, starvation and priority inversion

Define livelockThreads keep backing off and retrying in step. The CPU is busy, and nothing moves.
Break a livelockAdd randomized backoff, or give one side priority.
Define starvationOne thread waits forever while others keep winning the lock.
Stop a thread starvingnew ReentrantLock(true)A fair lock serves waiters in order, at some cost to throughput.
Define priority inversionA low-priority thread holds a lock a high one needs, and a medium one preempts it.
Prevent priority inversionPriority inheritance runs the holder at the waiter's priority.

TipMars Pathfinder kept resetting because of priority inversion.

TrapPriority inheritance helps only direct lock waits. It can't help a task stuck on I/O or a full queue.

Waiting & signaling

sleep until something changes, or cap who gets in

Condition variables

Consumer, in Pythonwith cond:takes the lock while not items:recheck after waking cond.wait()unlocks while asleep item = items.pop()Producerwith cond: items.append(x); cond.notify()
Wait for a state change without spinningWait on a condition variable. It sleeps until another thread signals.
Say why it pairs with a mutexWaiting releases the mutex as the thread sleeps. Waking takes it back.

TrapRecheck in a while, never an if. Wakeups can be spurious, or another thread got in first.

Semaphore or mutex

Let at most 10 threads use a resource at onceSemaphore slots = new Semaphore(10);acquire() waits when no permit is left.
Signal another threadRelease a permit it waits on.

TrapA semaphore has no owner, so any thread can release it. As a lock, it loses that check.

Producer-consumer and bounded buffers

Define the producer-consumer problemProducers fill a shared buffer and consumers drain it, at different speeds.
Block when the buffer is full or emptyOne mutex and two condition variables, not full and not empty.
Use a ready-made bounded buffernew ArrayBlockingQueue<>(100)put blocks when it's full, and take when it's empty.

TipTwo counting semaphores work too, one for free slots and one for filled ones.

Atomics & memory visibility

when one thread sees another thread's writes

Atomic operations

Bump a counter without a lockhits.incrementAndGet();Read, add and write happen as one step.
Say when one atomic is enoughOne variable, and each update stands alone, like a hit counter.
Say when you need a lock insteadTwo variables change together, or you act on a value later.
Tell another thread to stopvolatile boolean stop;Java's volatile is enough. In C++ it isn't, so use std::atomic.

TrapCheck-then-act on an atomic flag still races. Each access is atomic, but the sequence isn't.

Visibility and memory ordering

Writer, in C++data = 42;ready.store(true, std::memory_order_release);Readerif (ready.load(std::memory_order_acquire)) use(data);sees 42
Say why a thread misses another's writeThe write can sit in a core's store buffer, or the reader keeps a copy in a register.
Make a write visibleUse a lock, an atomic or a memory barrier. Nothing else promises it.
Publish data to another threadWrite the data, then do a release store to a flag.
Read the published dataDo an acquire load of the flag. If it's set, the data is visible.
Say when relaxed ordering is safeOnly when no thread relies on it to see other data.

TipCaches stay coherent. The delay is in the store buffer.

TrapA missing acquire or release fails mostly on ARM. On x86, only the compiler can reorder those writes.

Double-checked locking and singletons

Say how double-checked locking breaksThe pointer can be published before the object's fields. Readers see it half-built.
Fix it in Javaprivate static volatile Config instance;volatile adds the ordering it needs.
Lazy-init a singleton on the JVMUse a holder class. Class loading builds it once, and reads need no barrier.
Lazy-init a singleton in C++static Config& get() { static Config c; return c; }The compiler makes its first init thread-safe.

TrapBroken double-checked locking passes every test on x86, then fails on ARM or after a compiler upgrade.

CAS & lock-free code

update shared data without taking a lock

Compare-and-swap

Say what CAS doesIt writes the new value only if the old one is still there. If not, it fails and you retry.
Build a lock-free counterdo { n = c.get(); } while (!c.compareAndSet(n, n + 1));
Define lock-freeSome thread always makes progress, but one can retry forever.
Define wait-freeEvery operation finishes in a bounded number of its own steps.

TipA stalled thread can't block either kind, as a stalled lock holder can. Wait-free is rarer and often slower.

TrapUnder heavy contention, failed retries burn CPU and can lose to a plain lock.

ABA and freeing nodes

thread 1read ApausedCAS okthread 2pop Apop Bpush AheadABCAB, freedtime

Thread 1 reads head A, then pauses. Thread 2 pops A and B, then pushes A back. Thread 1's CAS still sees A, succeeds, and sets head to freed node B.

Explain the ABA problemThe value goes A to B to A between your read and your CAS, so the CAS succeeds.
Fix ABA with a version tagPair the pointer with a counter bumped on every update. A double-width CAS swaps both.
Free a node no thread still readsHazard pointers. Each thread publishes what it's reading, and reclaim skips those.
Free removed nodes in batchesUse epochs. Free a batch once every thread has left that epoch. RCU works the same way.

TrapOne stalled thread pins its epoch and stops all reclamation, so memory grows without bound.

A queue for many producers

Build a lock-free queueThe Michael-Scott queue. Producers CAS the tail forward.
Scale past one hot tailShard into several queues. You lose one global order.
Shrink the contended stepA ring buffer like the LMAX Disruptor. One CAS claims a slot.

TrapEvery producer still fights over one tail cache line, so throughput flattens past a few cores.

Thread pools & backpressure

reuse threads, and stop work piling up

Thread cost and thread pools

Know what a new thread costsTens of microseconds to create and destroy, for the stack and kernel setup.
Know how big a thread's stack is0.5 to 8 MB of reserved address space, depending on the platform.
Run many short tasksExecutors.newFixedThreadPool(8)Workers reuse threads and pull tasks from one queue.
Size a poolAbout the core count for CPU work. More for I/O, where threads wait.

TipA pool also caps concurrency. A burst waits in the queue, not as new threads.

Bounded queues and backpressure

Say what an unbounded work queue doesIt hides overload. p99 climbs, GC pauses grow, then memory runs out.
Make a fast producer slow downPut a bounded queue between stages. When it's full, block, reject or drop.
Let the consumer set the paceCredit-based flow control. The consumer grants N items at a time.
Handle a source that can't slow downShed load. You can't make network packets wait.

TipBackpressure must reach the true source. One unbounded buffer upstream just moves the pile-up.

TrapExecutors.newFixedThreadPool queues without bound. Pass a bounded queue to ThreadPoolExecutor.

Nested tasks in one pool

Explain how nested tasks deadlock a poolEvery worker waits on a subtask queued to the same pool. No thread is left to run it.
Avoid the deadlockChain futures with continuations instead of blocking.
Block safely when you mustRun the nested stage on a separate pool.

TipA work-stealing pool can run the joined subtask on the waiting thread.

TrapA fixed pool of N threads deadlocks once tasks nest N deep.

Async & the event loop

one thread, many waits

The event loop

Say how one thread runs async workThe runtime does the waiting. Finished operations queue callbacks, and the loop runs one at a time.
Say when a ready callback runsAfter the current code returns and the stack is empty.
Know what a 200 ms sync call costsEvery pending request waits 200 ms too, and timers fire late.
Name what usually blocks the loopBig JSON.parse calls, sync crypto, long loops, regex backtracking.
See a blocked loop before users doconst h = monitorEventLoopDelay();h.enable();Import it from node:perf_hooks. It measures event-loop lag.

TrapA blocked loop misses health checks, so the orchestrator restarts the instance. Now it's a crash loop.

Async I/O or threads

Serve thousands of slow network callsAsync I/O. One thread juggles them, since waiting costs almost nothing.
Explain why async doesn't speed up CPU workPure computation never waits, so there's nothing to yield.
Run CPU-heavy work in Nodenew Worker('./work.js')Worker threads run it on other cores.

TipUse a thread pool for libraries that only block.

TrapAsync spreads logic across callbacks and blurs stack traces. For modest concurrency, threads are simpler.

Cancel async work cleanly

Cancel a request and everything under itfetch(url, { signal: ac.signal })ac.abort()Pass one AbortController signal down to every child task.
Handle completion racing cancellationSettle exactly once. Guard it with a done flag.
Clean up on every exitClear timers and listeners in finally. Make cleanup safe to repeat.

TrapA promise that never settles after abort hangs its awaiters forever. Reject pending waiters on cancel.

Actors instead of locks

Share no state at allUse actors. Each owns its state and handles one message at a time.
Say when actors fitMany independent parts with their own state, as in Erlang or Akka.

TipIn Erlang, a supervisor restarts an actor that crashes.

TrapActors can still deadlock through full mailboxes, and messages can arrive in a bad order.

Scaling under contention

why more threads can make a program slower

Contention and the knee

Say why one lock caps throughputThreads queue for it. By Amdahl's law, the serial part caps speedup.
Say why more threads can be slowerEach contended handoff costs a context switch and moves cache lines.
Spot a lock convoyThreads wake, miss the lock and sleep again. Lots of switching, little work.
Know a switch's cost under contentionA few microseconds, plus tens more refilling caches.
Find the kneePlot throughput against threads. CPU-bound work often peaks near the core count.

TipThe Universal Scalability Law predicts a peak, then a decline, as coherency cost grows quadratically.

TrapPast the knee, adding threads lowers throughput. Shrink the critical section or shard the lock instead.

Lock granularity and striping

Use one lock for the whole structureCoarse-grained. Simple and cheap, but every operation queues.
Use a lock per bucket or nodeFine-grained. More parallel, but more memory, overhead and deadlock risk.
Stripe a hot locklocks[hash & mask].lock();Use N locks, a power of two near the core count. The mask is one less than N.
Run an operation across all stripesTake the locks in index order. Sum per-stripe counts for size.
Scale a locked LRU cacheStripe the keys. Use clock eviction, since LRU writes on every hit.

TipStart coarse. Measure where threads collide, and split only those spots.

TrapA single hot key still serializes on its one stripe.

Hot counters and false sharing

Count across threads with little contentionLongAdder hits = new LongAdder();Each thread bumps its own cell. Reads sum them.
Know what reading it costsO(n) and slightly stale. Fine for metrics, wrong for hard limits.
Define false sharingThreads write different variables on one cache line, so the line bounces between cores.
Fix false sharing in C++alignas(64) std::atomic<long> count;Give each hot variable its own 64-byte line.

TrapUnpadded per-thread cells false-share, and the contention you removed comes back.

Finding concurrency bugs

catch races you can't reproduce on demand

Diagnose and stress-test a race

Turn a sometimes-wrong result into a traceAssert invariants on shared state. Log thread ID and time on failure.
Catch a race that didn't firego test -race ./...A thread sanitizer flags unsynchronized access even if the output looks fine.
Make a rare interleaving happenAdd random sleeps and yields. Run many threads on few cores.
Try every interleavingA model checker steps through the schedules one by one.
Reproduce production loadShadow or replay real traffic against an instrumented build.
Confirm a suspect variableAdd a lock and watch the symptom vanish.

TrapLogging and sleeps change the timing. A passing instrumented run is weak proof.

Review code for concurrency bugs

Start a reviewList shared mutable state. For each field, ask who writes it, who reads it, and under which lock.
Check fields that change togetherChange them under one lock, or a reader sees one without the other.
Find a deadlock by readingFlag any two locks taken in both orders across functions.

TipReason about the worst interleaving. Ask what happens if a switch lands on this exact line.

Found this useful? Pass it on.
Pro · $10/mo

This sheet stays free. With Pro, you can:

  • Every answer on this sheet. Read the full answer to all 54 questions it links, not just the 7 free ones.
  • My sheet. The cards you star and the ones you miss in Quiz me, from every sheet, on one page to quiz and print.
  • PDFs. Download this sheet and My sheet, laid out for A4, with every question a link to its answer.
  • The rest of Pro. Every answer in the question library, and 1,000 AI credits a month.