Last Updated: February 2, 2026
The Building H2O Problem is a beautiful synchronization puzzle. Hydrogen and oxygen threads arrive in arbitrary order and must combine in groups of exactly two hydrogens and one oxygen to form water molecules.
What makes this problem compelling is its precision requirement. Two hydrogen threads must wait for exactly one oxygen thread, and vice versa.
This pattern appears throughout computing: batch processing systems that need fixed group sizes, request aggregation that combines exactly N requests, and resource allocation that requires specific resource combinations.
There are two kinds of threads: hydrogen and oxygen. Your goal is to form water molecules by combining them. Each water molecule requires exactly two hydrogen atoms and one oxygen atom. Threads arrive concurrently and must synchronize to form complete molecules.
The challenge is making sure no thread proceeds until its molecule partners are ready. You can't have a hydrogen wandering off alone, and you can't have two oxygens trying to share the same hydrogens.
Let's understand the entities involved in this problem.
The analogy to real systems is direct: hydrogens are like individual requests waiting to be batched, oxygens are like batch coordinators, and molecule formation is like batch processing where exactly N items must be ready.
These constraints define correct behavior.
That last rule deserves emphasis. We're forming molecules one at a time. If 10 hydrogens and 5 oxygens arrive, we form 5 molecules sequentially, not simultaneously. Some variations allow parallel molecule formation, but the basic problem is sequential.
With setup and rules clear, here's what our solution must achieve.
Hydrogen and oxygen threads wait in a pool. When two hydrogens and one oxygen are available, they combine to form a molecule. The remaining threads (H3, O2) wait for more atoms to arrive before forming the next molecule.
The H2O problem exemplifies barrier-based synchronization with ratio constraints. Many real-world systems need to group items in specific ratios before processing.
Think about these scenarios:
In all these cases, progress requires precise grouping. One missing element means everyone waits.
Before jumping into solutions, let's understand what makes this problem hard.
We need exactly 2H + 1O. Not "at least" but "exactly." If only 1H and 1O arrive, neither should proceed. They must wait for the second hydrogen. If 3H and 1O arrive, only 2H should proceed with that O. The third H waits for future atoms.
The challenge is tracking these counts accurately and releasing exactly the right threads when the ratio is satisfied. Too many, and we've broken the molecule formula. Too few, and we have leftover atoms.
This is subtle but critical. Suppose 2H arrive and start "forming" a molecule, but O hasn't arrived yet. We can't have those 2H declare themselves part of a molecule and proceed. All three must synchronize at the same point before any can claim to have formed a molecule.
Why does this matter? Because the molecule formation isn't just about counting. It's about ensuring all three threads pass through a synchronization point together. If H1 runs ahead, it might interfere with the next molecule's formation.
With many threads arriving (e.g., 10H and 5O), we need to form 5 molecules. We can't have all 10H trying to participate in the first molecule. After 2H join a molecule, the next 2H should form the next molecule.
This requires some form of admission control. We limit how many H and O can be "actively trying" to form a molecule at once. Semaphores are perfect for this.
With these challenges in mind, here's how we'll evaluate each solution.
| Property | Definition | Why It Matters |
|---|---|---|
| Correct ratio | Exactly 2H + 1O per molecule | Functional correctness |
| Deadlock-free | System never freezes | Liveness |
| Starvation-free | Every thread eventually proceeds | Fairness |
| Efficient | Minimal unnecessary blocking | Performance |
Now let's see how different approaches handle these challenges.
The intuitive approach: use counters to track waiting hydrogens and oxygens. When counts reach 2H and 1O, form a molecule.
The idea seems straightforward.
| Property | Status | Explanation |
|---|---|---|
| Correct ratio | No | Race condition on counter decrement |
| Deadlock-free | Maybe | Depends on thread interleaving |
| Starvation-free | No | No fairness guarantee |
| Efficient | Low | notifyAll wakes all threads |
This solution has a critical flaw: when 2H and 1O all wake up, they each try to decrement their counters and proceed. But there's no coordination to ensure exactly 2H and exactly 1O proceed.
Imagine this scenario:
Or worse: multiple oxygens might all see the condition as true and all decrement, leading to more than one O per molecule.
The fundamental issue is that checking and decrementing aren't atomic across all three threads. We need a synchronization point where all three arrive before any proceed. That's what barriers are for.
The key insight is that molecule formation is a synchronization barrier: all three threads (2H + 1O) must arrive at the barrier before any can pass. Combined with semaphores to control who can reach the barrier, this gives us a clean solution.
We need two mechanisms working together.
Semaphores for admission control: Allow only 2H and 1O to try forming a molecule at a time. Initialize hydrogen semaphore to 2, oxygen semaphore to 1. Each thread acquires its semaphore before proceeding.
Barrier for synchronization: Requires 3 threads to proceed. When 2H + 1O arrive at the barrier, all three pass simultaneously. The barrier callback releases new semaphore permits for the next molecule.
Semaphores ensure the right ratio (2H + 1O). The barrier ensures they all proceed together. The callback ensures the next molecule waits for fresh permits.
Here's the flow.
Notice the layered approach. Semaphores filter who can try (admission). Barrier synchronizes who can proceed (coordination). Together they give us exact 2H + 1O grouping.
Now let's implement this properly.
Let's trace through exactly what happens when we have H1, H2, and O1 arriving.
The beauty is in the separation of concerns. Semaphores handle the ratio. Barrier handles the synchronization. Callback handles the reset.
| Property | Status | Explanation |
|---|---|---|
| Correct ratio | Yes | Semaphores enforce 2H + 1O admission |
| Deadlock-free | Yes | Barrier guarantees all proceed together |
| Starvation-free | Depends | Depends on semaphore fairness |
| Efficient | High | Only 3 threads blocked per molecule |
The semaphores act as admission control: only 2H and 1O can acquire permits and reach the barrier. Once 3 threads (exactly 2H + 1O) are at the barrier, it breaks, all proceed, and the barrier callback releases permits for the next molecule. The CyclicBarrier resets automatically for reuse.
Note where the callbacks run: in the barrier completion action, not immediately after each thread passes. This ensures the next molecule only starts after the current one is fully complete.
The barrier-based solution is elegant, but what if you're in an environment without built-in barriers? That's where our next approach comes in.
For environments without barriers, we can build the same logic using only semaphores. This solution is more portable and demonstrates deeper understanding of semaphore semantics.
We can simulate a barrier using counting semaphores. The idea is that threads wait on a "proceed" semaphore. When the required count is reached, someone releases permits for all waiting threads.
The trick is deciding who triggers the release. We use a mutex-protected counter to track arrivals. Whichever thread brings the count to the required total releases permits for everyone (including itself).
Here's how it works.
The key insight is that the thread completing the group (bringing total to 2H + 1O) acts as the coordinator, releasing all three threads.
Let's trace through the key synchronization points.
| Property | Status | Explanation |
|---|---|---|
| Correct ratio | Yes | Exactly 2H and 1O permits released |
| Deadlock-free | Yes | Mutex released before blocking wait |
| Starvation-free | Depends | FIFO semaphores help |
| Efficient | High | Only necessary threads blocked |
| Approach | Correct | Deadlock-free | Complexity | Best For |
|---|---|---|---|---|
| Naive (counters) | No | Risky | Simple | Understanding the problem |
| Barrier + Semaphore | Yes | Yes | Medium | Java (has CyclicBarrier) |
| Semaphore-only | Yes | Yes | Medium | Any language |
Use the barrier-based solution in Java interviews as it's the most elegant and shows you know the right abstractions. For other languages without built-in barriers, the semaphore-only solution demonstrates deeper understanding of semaphore semantics.