CMD Guide
HomeConcurrencyConcurrency Problems

Problem 12 Building H2O

The invariant, and why a permit leak breaks it

Hydrogen and oxygen threads arrive in any order; you must let them through only in complete molecules — two H and one O bond together, and no extra hydrogen may slip ahead. Two mechanisms compose to enforce this: counting semaphores cap how many of each element can stage (2 H, 1 O), and a barrier of 3 makes the staged 2H+1O wait for each other before any of them "bonds" (prints). The old version released more permits than it took (oxygen did hSem.release(2) while each hydrogen also released), so the hydrogen permit count drifted upward and more than two H could pass between oxygens — the invariant silently broke.

Two hydrogen permits and one oxygen permit feed a 3-party barrier; only when 2H+1O have arrived do all three release together, forming one molecule
Two hydrogen permits and one oxygen permit feed a 3-party barrier; only when 2H+1O have arrived do all three release together, forming one molecule

Correct Java

import java.util.concurrent.Semaphore;
import java.util.concurrent.CyclicBarrier;

class H2O {
    private final Semaphore hSem = new Semaphore(2);   // at most 2 H staged
    private final Semaphore oSem = new Semaphore(1);   // at most 1 O staged
    private final CyclicBarrier barrier = new CyclicBarrier(3); // 2H + 1O assemble

    public void hydrogen(Runnable releaseHydrogen) throws InterruptedException {
        hSem.acquire();
        await();                       // wait for the full molecule
        releaseHydrogen.run();
        hSem.release();                // release ONLY my own permit
    }
    public void oxygen(Runnable releaseOxygen) throws InterruptedException {
        oSem.acquire();
        await();
        releaseOxygen.run();
        oSem.release();
    }
    private void await() {
        try { barrier.await(); }
        catch (Exception e) { Thread.currentThread().interrupt(); }
    }
}

The barrier auto-resets after every group of 3, so molecule after molecule forms correctly, and each thread releases exactly the one permit it acquired — the counts never drift.

Correct Go (buffered channels as semaphores + a reusable barrier)

Go has no built-in CyclicBarrier, so we build a tiny reusable one from a mutex + condition; the semaphores are buffered channels (a slot = a permit).

type H2O struct {
    h chan struct{} // cap 2
    o chan struct{} // cap 1
    mu sync.Mutex; cond *sync.Cond; count int
}
func New() *H2O { x := &H2O{h: make(chan struct{},2), o: make(chan struct{},1)}; x.cond = sync.NewCond(&x.mu); return x }

func (x *H2O) barrier() {            // releases in groups of 3
    x.mu.Lock(); x.count++
    if x.count%3 == 0 { x.cond.Broadcast() } else { for x.count%3 != 0 { x.cond.Wait() } }
    x.mu.Unlock()
}
func (x *H2O) Hydrogen(release func()) { x.h <- struct{}{}; x.barrier(); release(); <-x.h }
func (x *H2O) Oxygen(release func())   { x.o <- struct{}{}; x.barrier(); release(); <-x.o }

Why the counts do the real work — a worked leak

The whole design rests on one arithmetic fact: the barrier trips at exactly 3 arrivals, and the only way to get 3 arrivals is 2 H + 1 O — because hSem caps concurrent hydrogens at 2 and oSem caps oxygens at 1 (2 + 1 = 3). The semaphore counts are not a throttle for performance; they are what types the barrier's three slots. Break the count and a barrier of 3 will happily trip on the wrong composition. The old bug had oxygen call hSem.release(2) in addition to each hydrogen releasing its own — so hSem inflated past 2. Trace it:

#EventhSemBarrier arrivals
1H1 acquire, H2 acquire, O acquire02H + 1O → trips (correct molecule)
2bonding: H1 release, H2 release2
3O also does hSem.release(2) (the bug)4
4H3, H4, H5 all acquire (4 permits available)1H3 + H4 + H5 → trips with 3 H, 0 O

Step 4 is the invariant break made concrete: with hSem > 2, three hydrogens satisfy the barrier's party count by themselves and "bond" into an HHH molecule with no oxygen present. The failure is silent — the program never throws, it just emits chemically impossible output. That is the signature of a count bug: correctness is decided by an integer you have to keep exactly right, and the barrier faithfully enforces whatever (wrong) grouping the counts allow.

Pitfalls

try {
    barrier.await();
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();
    // barrier is now broken for others — stop producing molecules
    throw e;
} catch (BrokenBarrierException e) {
    // peer failed; do not release() as if molecule formed
    throw new IllegalStateException("molecule assembly aborted", e);
}

When a barrier is the wrong tool

CyclicBarrier is the right primitive here precisely because the party count is fixed (always 3) and the rendezvous repeats molecule after molecule. Reach for something else when either assumption breaks: for a one-shot rendezvous with no reuse, CountDownLatch is lighter; when parties join or drop out dynamically, use Phaser (its register / arriveAndDeregister adjust the party count at runtime, which a barrier cannot). And never substitute a bare mutex + counter with no reset protocol — if the third thread never arrives, the first two are parked forever with no way to rearm the group.

Why not a single lock + counters, then? You can solve H2O with one mutex, an h/o counter, and a condition variable: a hydrogen blocks until h < 2, increments, and when h == 2 && o == 1 some thread signals the trio to proceed and resets the counters. It works — but you have just hand-rolled the semaphore-cap-plus-barrier out of raw parts, and the reset step (who zeroes the counters, and when, so a late thread from molecule k can't be counted into molecule k+1) is exactly the error-prone bookkeeping the barrier gives you for free with its automatic re-arm. Prefer the two composed primitives: the counts and the rendezvous are each expressed declaratively, so there is no reset protocol left to get wrong. Drop to the manual monitor only when the grouping rule is too irregular for a fixed party count (e.g. "bond whatever is staged every 100ms").

Where this pattern generalizes. H2O is one instance of assemble a fixed heterogeneous group of N before anyone proceeds: per-type semaphores type the members, a barrier of N does the rendezvous. The same shape appears in request batching (hold callers until a batch of N is staged, then flush together), phased parallel algorithms (every worker must finish iteration i before any starts i+1 — a barrier of worker-count), and staged pipelines that need a full crew before a stage runs. The tell that this pattern fits: the group size is fixed and known, and progress is all-or-nothing for the group. When the count varies at runtime, that fixed party count is exactly what forces you off a barrier and onto a Phaser or a manual monitor.

Takeaways


Re-authored for correctness for this guide (the prior version leaked semaphore permits, breaking the 2:1 ratio). Pattern: LeetCode 1117 "Building H2O". See also: Semaphore, Barriers, Condition Variables.

🔨 Practice this hands-on — Build the H2O Molecule Barrier →
Attempt it from an empty file, break it to feel the failure, then defend it under pushback.
🤖 Don't fully get this? Learn it with Claude

Stuck on Problem 12 Building H2O? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.

🪜 Hint ladder (no spoilers)

Progressively stronger hints — you still solve it.

I'm working on the problem **Problem 12 Building H2O** (Concurrency). Give me a HINT LADDER: start with the tiniest nudge, then wait. Only reveal the next, stronger hint when I ask. Do NOT show the full solution unless I type 'show solution'. Keep me doing the thinking. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🎨 Explain the approach visually

See the technique, not just code.

Explain the optimal approach to **Problem 12 Building H2O** with a VISUAL walkthrough: trace it on a small concrete example using ASCII art / a step-by-step diagram, narrate what changes each step, then give time & space complexity with a one-line derivation. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🔍 Review my solution

Catch bugs, edge cases, sub-optimality.

I'll paste my solution to **Problem 12 Building H2O**. Review it for correctness, missed edge cases, and time/space complexity, then coach me toward the optimal — don't just rewrite it. Ask me to paste my code now. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🔁 Drill the pattern

Lock in recognition with look-alikes.

Give me 2 problems that use the SAME underlying pattern as **Problem 12 Building H2O**. For each, let me attempt first, then review my answer and name the trigger signal that reveals the pattern. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.

📝 My notes