CMD Guide
HomeConcurrency

Concurrency Problems

Step 2 in the Concurrency path · 6 concepts · 18 problems

0 / 24 complete

📘 Learn Concurrency Problems from zero

Concurrency problems reward a calm, disciplined process far more than cleverness. Use the same loop every time.

  1. Read & restate. Say out loud what the threads are and what each must do. The decisive question is always: what state is shared, and what ordering must hold? Most "hard" concurrency problems become easy once you name these two things.
  2. Pin the constraints. Number of threads, fixed vs. dynamic? Is output order required, or just correctness? Bounded resources (buffer size, N philosophers)? Termination condition? These dictate the primitive.
  3. Brute force first — and make it correct, not fast. Start with one big lock (synchronized) around all shared state. A correct serialized version is your baseline; never optimize before it works. This also exposes exactly which sections actually contend.
  4. Spot the pattern. Map the problem to a canonical shape: mutual exclusion (locks/atomics), signaling/ordering (semaphores, condition variables, latches), rendezvous (barriers), producer–consumer (blocking queues), or deadlock avoidance (lock ordering / arbiter). Name it before you code.
  5. Optimize deliberately. Shrink critical sections, swap a global lock for fine-grained locks or lock-free atomics, partition work across a pool and reduce. Each step trades simplicity for throughput — justify it.
  6. Edge cases & correctness checks. Spurious wakeups (always re-check the condition in a while loop, never an if), deadlock (hold-and-wait + circular wait), livelock, starvation/fairness, and clean shutdown. Reason about the worst interleaving, not the happy path.
  7. Test. Run with many threads and high iteration counts; concurrency bugs are probabilistic. State your invariant ("at most N at the table," "H:O is 2:1 in every prefix") and argue why no interleaving breaks it.

Be encouraged: the primitive almost always falls out of step 4 once you've honestly answered step 1.

✨ Added by the guide to build intuition — not from the source course.

🎯 Guided practice

Worked example: Building H2O (Problem 12). Hydrogen and oxygen threads each call a hydrogen() / oxygen() method that takes a "release" runnable; you must release them in groups that form one water molecule — exactly 2 H and 1 O bonding together before any thread of the next molecule proceeds.

Step 1 — name the shared state and ordering. Two requirements jump out: (a) a ratio constraint — never let 3 H bond, or 2 O, in one group; and (b) a rendezvous — the three threads of one molecule must all arrive before any are released. That second word, "rendezvous," is the loudest signal.

Step 2 — match signal to technique. "A group of fixed size must all arrive, then proceed together" is the textbook trigger for a barrier (CyclicBarrier(3)): when the third thread arrives, all three cross. But the barrier alone can't enforce the 2:1 ratio — without admission control, three hydrogens could pile into the same barrier. So we add counting semaphores as admission gates: hSem with 2 permits and oSem with 1 permit. This pairing — a semaphore for "how many of each may enter," a barrier for "all bond at once" — is the canonical mutual-exclusion-plus-rendezvous combination.

Step 3 — outline the optimal.

  1. Init Semaphore hSem = new Semaphore(2), Semaphore oSem = new Semaphore(1), CyclicBarrier barrier = new CyclicBarrier(3).
  2. hydrogen(): hSem.acquire()barrier.await()releaseHydrogen.run()hSem.release().
  3. oxygen(): oSem.acquire()barrier.await()releaseOxygen.run()oSem.release().

Emit the atom after barrier.await() returns, not before — that is the canonical accepted form, because the bond is printed only once the full molecule is confirmed present, so the output reads as one molecule at a time.

Why it's correct: the semaphores cap concurrent entrants at exactly 2 H and 1 O, so only one molecule's worth of threads can be past acquire() at a time. barrier.await() blocks until all three of that group are present — guaranteeing they bond as a set — and CyclicBarrier auto-resets the moment it trips. Permits are released only after await() returns, so a thread of the next molecule can't acquire until the current three have crossed. The invariant ("at most 2 H and 1 O between acquire and release") holds under every interleaving. The teaching point: when one constraint is "ratio/count" and another is "all-arrive-together," don't force one primitive to do both — layer a semaphore (admission) under a barrier (rendezvous).

✨ Added by the guide — work these before the full problem set.

Lessons in this topic

🧠 Review & recall

Active recall is what moves a topic into long-term memory. Flip each card before revealing, then test yourself — your results are saved on this device.

Flashcard
What 7-step disciplined process does the topic recommend for any concurrency problem, and what is the single most decisive question to answer first?
tap to reveal →
Read & restate, pin the constraints, brute-force first (one big lock, correct not fast), spot the canonical pattern, optimize deliberately, handle edge cases, then test with many threads/high iterations. The decisive first question is always: what state is shared, and what ordering must hold?
💡 Shared state + ordering = the whole game.
Flashcard
For Problem 1 (parallel linear search for one occurrence), what are the two division strategies and how is the shared result kept thread-safe?
tap to reveal →
Block division gives each thread a contiguous chunk (thread 0 -> indices 0-24, etc.); cyclic division hands out indices round-robin (thread 0 -> 0,4,8...). A shared volatile foundIndex starts at -1 and is updated only inside a synchronized(mtx) block, with early-exit once any thread has set it.
💡 Block = slabs, Cyclic = stripes; one mutex guards foundIndex.
Flashcard
How do you parallelize MinMaxSum (Problem 4), and what pattern is this an instance of?
tap to reveal →
Split the array into chunks, have each thread compute a partial min/max/sum on its chunk into a per-thread results slot, join all threads, then combine the partials into the global result. It is the map-reduce / partition-then-aggregate pattern, needing no lock when each thread writes its own slot.
💡 Map each chunk, reduce the partials.
Flashcard
What primitives solve 'Print in Order' (Problem 9) so first() -> second() -> third() regardless of thread start order?
tap to reveal →
Two signaling gates initialized to block: semaphores semFirstDone=0 and semSecondDone=0 (or two CountDownLatch(1)s, firstDone and secondDone). second() waits on the first gate and third() waits on the second; each method signals/counts down its gate when done, enforcing the chain.
💡 Each step opens the gate the next step waits on.
Flashcard
In FizzBuzz / ZeroEvenOdd (Problems 7-8), why must each thread re-check its condition in a while loop and not an if, after waiting on a condition variable?
tap to reveal →
A ReentrantLock + Condition lets a thread await() until its turn (e.g. number divisible by 3, or zeroTurn==true), and signalAll wakes everyone. Because of spurious wakeups and multiple waiters, the woken thread must re-test the predicate in a while loop before acting; an if could let it proceed when the condition is actually false.
💡 Always while-re-check the condition, never if.
Flashcard
How is deadlock avoided in the Dining Philosophers (Problem 15)?
tap to reveal →
Each fork is a mutex; a philosopher needs both neighbors' forks to eat. Deadlock (everyone grabs the left fork and waits forever for the right) is broken with asymmetric lock ordering: philosophers 0..N-2 take left-then-right while the last philosopher takes right-then-left, eliminating the circular-wait condition.
💡 Make one philosopher grab forks backwards -> break the cycle.
Flashcard
In the multithreaded web crawler (Problem 16), what shared state needs protection and how?
tap to reveal →
The to-visit queue (toVisitList) and the visited set (visitedRecord) are shared. They are guarded by two mutexes (queueLock and visitedLock) so only one agent at a time pops a URL or marks it visited, preventing two threads from processing the same URL (duplicate work) and preserving data integrity.
💡 Two locks: one for the queue, one for the visited set.
Q1. In Problem 12 (Building H2O), the guide's accepted solution layers two primitives. Which combination and why?
Q2. Why does cyclic (round-robin) index division reduce contention versus block division in the parallel linear search?
Q3. In 'Print in Order', the two semaphores semFirstDone and semSecondDone are both initialized to 0. What does that accomplish?
Q4. What is the correct termination/edge-case discipline emphasized for these problems?
Q5. In the multithreaded web crawler, what failure does protecting toVisitList and visitedRecord with mutexes specifically prevent?