# 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).