synchronizers: CountDownLatch vs CyclicBarrier vs Phaser vs Semaphore
One three-round worker pipeline implemented with CountDownLatch (one latch per round), CyclicBarrier (reusable, with a barrier action), and Phaser (reusable, plus dynamic mid-run registration), alongside Semaphore solving the genuinely different problem of bounding concurrent access. Covers timeout behavior (CyclicBarrier permanently breaks on one timeout, Phaser doesn't) and virtual-thread compatibility, including the corrected finding that Object.wait() releases its monitor and never pinned, unlike Thread.sleep() inside synchronized. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01FhzLY5p6okFva3qsnsRyvM
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
# synchronizers
|
||||
|
||||
Companion code for the ankurm.com post *"CountDownLatch vs CyclicBarrier vs Phaser vs Semaphore
|
||||
in Java."* Fifth module in `java-core-examples`, the Java-core / concurrency series.
|
||||
|
||||
The same three-round worker pipeline, implemented once per class where it's a natural fit
|
||||
(`CountDownLatch`, `CyclicBarrier`, `Phaser`), plus `Semaphore` solving the genuinely different
|
||||
problem it actually solves (bounding concurrent access, not waiting for everyone to arrive) inside
|
||||
the same pipeline shape. Reuse semantics, timeout behavior, and virtual-thread compatibility are
|
||||
each demonstrated directly rather than asserted.
|
||||
|
||||
## Versions this was built and tested against
|
||||
|
||||
| Component | Version | Notes |
|
||||
|---|---|---|
|
||||
| JDK (primary) | 25.0.4.1+1 (Temurin, LTS) | |
|
||||
| JDK (comparison) | 21.0.10 (OpenJDK) | For the virtual-thread pinning contrast only. |
|
||||
| JUnit Jupiter | 5.11.0 | Correctness tests, 20 repeats per barrier test. |
|
||||
| Maven | 3.9.11 | |
|
||||
| Hardware | 2 vCPU x86-64 VM | Same sandbox as the rest of this series. |
|
||||
|
||||
## Quickstart
|
||||
|
||||
```bash
|
||||
export JAVA_HOME=/path/to/jdk-25
|
||||
mvn package
|
||||
java -cp target/classes com.ankurm.synchronizers.CyclicBarrierPipeline
|
||||
java -cp target/classes com.ankurm.synchronizers.PhaserPipeline
|
||||
java -cp target/classes com.ankurm.synchronizers.SemaphoreResourceGuard
|
||||
```
|
||||
|
||||
`scripts/run-all.sh` regenerates every file in `output/` (needs `JDK25_HOME` and `JDK21_HOME`).
|
||||
|
||||
## What's in here
|
||||
|
||||
| File | What it shows |
|
||||
|---|---|
|
||||
| `.../CountDownLatchPipeline.java` | A starting-gate latch, plus one fresh latch per round - the workaround `CyclicBarrier` exists to remove. |
|
||||
| `.../CyclicBarrierPipeline.java` | One reusable barrier across all three rounds, with a barrier action. |
|
||||
| `.../PhaserPipeline.java` | Same barrier behavior, plus a fifth worker dynamically registering after round 1. |
|
||||
| `.../SemaphoreResourceGuard.java` | The same pipeline, but `Semaphore` bounding concurrent access to a shared resource - a different concern from the other three. |
|
||||
| `.../TimeoutBehaviorDemo.java` | What each class does when the thing it's waiting for never arrives. |
|
||||
| `.../VirtualThreadCompatibilityDemo.java` | None of the four pin a virtual thread, even on JDK 21, because none are built on `synchronized`. |
|
||||
| `.../SynchronizedWaitPinsDemo.java` | The contrast case, and a correction: `synchronized` + `Object.wait()` does NOT pin (it releases the monitor); `synchronized` + `Thread.sleep()` does. |
|
||||
| `src/test/.../RoundOrderingCorrectnessTest.java` | Asserts no worker's round N+1 ever starts before every worker's round N finished (`CyclicBarrier`, `Phaser`, 20 repeats each), and `Semaphore` never exceeds its permit count. |
|
||||
| `output/01`-`04` | Each pipeline's real run. |
|
||||
| `output/05` | Timeout behavior across all four. |
|
||||
| `output/06` | Virtual-thread compatibility, JDK 21, both the four synchronizers and the `synchronized`+`wait`/`sleep` contrast. |
|
||||
| `output/07` | JUnit correctness run. |
|
||||
|
||||
## Reading the results honestly (2-vCPU sandbox)
|
||||
|
||||
**`Semaphore` isn't a fourth way to do what the other three do.** `CountDownLatch`, `CyclicBarrier`,
|
||||
and `Phaser` all answer "has everyone reached this point?" `Semaphore` answers "how many callers
|
||||
are allowed through at once?" - a genuinely different question, not a stylistic alternative.
|
||||
`output/04` shows `Semaphore` bounding a resource to 2 concurrent callers, independent of round
|
||||
boundaries or worker identity; forcing it into a "wait for the group" role would misrepresent what
|
||||
it's for.
|
||||
|
||||
**A timeout doesn't do the same thing on all four** (`output/05`). `CountDownLatch.await(timeout)`
|
||||
returns `false` and stays reusable to check again. `Semaphore.tryAcquire(timeout)` behaves the same
|
||||
way. `CyclicBarrier.await(timeout)` throws `TimeoutException` and **permanently breaks the barrier
|
||||
for every other waiter** until an explicit `reset()` - one straggler poisons the whole group.
|
||||
`Phaser.awaitAdvanceInterruptibly(phase, timeout, unit)` throws `TimeoutException` too, but the
|
||||
phaser itself is untouched and a later, fully-arrived round on the same instance succeeds normally
|
||||
with no reset needed. That difference alone is a reason to reach for `Phaser` over `CyclicBarrier`
|
||||
in anything where a slow round shouldn't take down every other round after it.
|
||||
|
||||
**`Object.wait()` doesn't pin a virtual thread - `Thread.sleep()` inside the same `synchronized`
|
||||
block does** (`output/06`), and this repo initially assumed otherwise before checking. `wait()`
|
||||
fully releases the monitor for the duration of the wait, so there's nothing held while parked and
|
||||
nothing to pin; JEP 491 (JDK 24) only needed to fix the case where a blocking call runs *while
|
||||
still holding* the monitor. All four `java.util.concurrent` classes in this module were already
|
||||
safe on virtual threads on JDK 21, years before JEP 491, because none of them are built on
|
||||
`synchronized` in the first place - a fact worth knowing before assuming every pre-JDK-24
|
||||
concurrency primitive needed the same fix.
|
||||
|
||||
## License
|
||||
|
||||
MIT - see the [repo-wide LICENSE](../LICENSE).
|
||||
Reference in New Issue
Block a user