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:
2026-09-30 06:43:52 +00:00
co-authored by Claude Sonnet 5
parent 0819993c51
commit 55ee850a75
20 changed files with 962 additions and 0 deletions
+1
View File
@@ -9,6 +9,7 @@ article; each module's own README has that article's version table, quickstart,
| [`locks`](locks/) | synchronized vs ReentrantLock vs StampedLock: Benchmarks and a Decision Table |
| [`atomics`](atomics/) | Java Atomics and VarHandle: CAS, LongAdder, and When Atomics Beat Locks |
| [`vt-pinning`](vt-pinning/) | Diagnosing Virtual Thread Pinning in Production: JFR Events, jcmd, and Real Fixes |
| [`synchronizers`](synchronizers/) | CountDownLatch vs CyclicBarrier vs Phaser vs Semaphore in Java |
## License
+1
View File
@@ -17,6 +17,7 @@
<module>locks</module>
<module>atomics</module>
<module>vt-pinning</module>
<module>synchronizers</module>
</modules>
<properties>
+80
View File
@@ -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).
@@ -0,0 +1,17 @@
$ java CountDownLatchPipeline
main: all 4 workers created, still waiting at the start gate
main: releasing the start gate
worker-0: finished round 1
worker-1: finished round 1
worker-2: finished round 1
worker-3: finished round 1
worker-0: finished round 2
worker-1: finished round 2
worker-2: finished round 2
worker-3: finished round 2
worker-0: finished round 3
worker-1: finished round 3
worker-2: finished round 3
worker-3: finished round 3
done
@@ -0,0 +1,18 @@
$ java CyclicBarrierPipeline
worker-0: finished round 1
worker-1: finished round 1
worker-2: finished round 1
worker-3: finished round 1
barrier action: round 1 complete, all 4 workers arrived
worker-0: finished round 2
worker-1: finished round 2
worker-2: finished round 2
worker-3: finished round 2
barrier action: round 2 complete, all 4 workers arrived
worker-0: finished round 3
worker-1: finished round 3
worker-2: finished round 3
worker-3: finished round 3
barrier action: round 3 complete, all 4 workers arrived
done
@@ -0,0 +1,21 @@
$ java PhaserPipeline
worker-0: finished round 1
worker-1: finished round 1
worker-2: finished round 1
worker-3: finished round 1
phaser onAdvance: phase 1 complete, 4 registered parties -> advancing
worker-4: registers after round 1 (phaser now has 5 parties)
worker-0: finished round 2
worker-1: finished round 2
worker-2: finished round 2
worker-3: finished round 2
worker-4: finished round 2
phaser onAdvance: phase 2 complete, 5 registered parties -> advancing
worker-0: finished round 3
worker-1: finished round 3
worker-2: finished round 3
worker-3: finished round 3
worker-4: finished round 3
phaser onAdvance: phase 3 complete, 5 registered parties -> advancing
done
@@ -0,0 +1,16 @@
$ java SemaphoreResourceGuard
worker-0 round 1: acquired permit, 1 callers concurrently in the resource (availablePermits=0)
worker-1 round 1: acquired permit, 2 callers concurrently in the resource (availablePermits=0)
worker-2 round 1: acquired permit, 2 callers concurrently in the resource (availablePermits=0)
worker-3 round 1: acquired permit, 2 callers concurrently in the resource (availablePermits=0)
worker-3 round 2: acquired permit, 1 callers concurrently in the resource (availablePermits=1)
worker-0 round 2: acquired permit, 2 callers concurrently in the resource (availablePermits=0)
worker-1 round 2: acquired permit, 1 callers concurrently in the resource (availablePermits=1)
worker-2 round 2: acquired permit, 2 callers concurrently in the resource (availablePermits=0)
worker-2 round 3: acquired permit, 1 callers concurrently in the resource (availablePermits=1)
worker-3 round 3: acquired permit, 2 callers concurrently in the resource (availablePermits=0)
worker-0 round 3: acquired permit, 1 callers concurrently in the resource (availablePermits=1)
worker-1 round 3: acquired permit, 2 callers concurrently in the resource (availablePermits=0)
permits=2 maxObservedConcurrency=2
done
@@ -0,0 +1,20 @@
$ java TimeoutBehaviorDemo
=== CountDownLatch.await(timeout): expects 2, only 1 ever counts down ===
await returned: false (false = timed out, latch still usable to re-check later)
getCount() after timeout: 1
=== CyclicBarrier.await(timeout): expects 2 parties, only 1 arrives ===
await threw TimeoutException, as expected
barrier.isBroken() after the timeout: true
a second, later await() on the SAME barrier: BrokenBarrierException immediately - one timeout poisons the barrier for everyone until reset()
=== Phaser.awaitAdvanceInterruptibly(phase, timeout): expects 2 parties, only 1 arrives ===
awaitAdvanceInterruptibly threw TimeoutException, as expected
phaser.getPhase() after the timeout: 0 (unchanged - still phase 0, not advanced or broken)
a later round on the SAME phaser, once both parties actually arrive: succeeds normally, now at phase 1
=== Semaphore.tryAcquire(timeout): 0 permits available ===
tryAcquire returned: false (false = timed out, no exception, no broken state)
a later tryAcquire after a release: true - fully independent of the earlier timeout
done
@@ -0,0 +1,25 @@
$ java -Djdk.tracePinnedThreads=full VirtualThreadCompatibilityDemo (pre-JDK-24 runtime)
java.version=21.0.10
jdk.tracePinnedThreads=full
CountDownLatch.await(): completed, no trace above this line for it
CyclicBarrier.await(): completed, no trace above this line for it
Phaser.arriveAndAwaitAdvance(): completed, no trace above this line for it
Semaphore.acquire(): completed, no trace above this line for it
done - no pinning trace printed above for any of the four
$ java -Djdk.tracePinnedThreads=full SynchronizedWaitPinsDemo (pre-JDK-24 runtime, contrast case)
java.version=21.0.10
jdk.tracePinnedThreads=full
synchronized + Object.wait(): completed, no pinning trace above this line for it
VirtualThread[#21]/runnable@ForkJoinPool-1-worker-1 reason:MONITOR
java.base/java.lang.VirtualThread$VThreadContinuation.onPinned(VirtualThread.java:199)
java.base/jdk.internal.vm.Continuation.onPinned0(Continuation.java:393)
java.base/java.lang.VirtualThread.parkNanos(VirtualThread.java:635)
java.base/java.lang.VirtualThread.sleepNanos(VirtualThread.java:807)
java.base/java.lang.Thread.sleep(Thread.java:507)
com.ankurm.synchronizers.SynchronizedWaitPinsDemo.lambda$main$1(SynchronizedWaitPinsDemo.java:48) <== monitors:1
java.base/java.lang.VirtualThread.run(VirtualThread.java:329)
synchronized + Thread.sleep(): completed, pinning trace (if any) appears above this line for it
done
@@ -0,0 +1,4 @@
-------------------------------------------------------------------------------
Test set: com.ankurm.synchronizers.RoundOrderingCorrectnessTest
-------------------------------------------------------------------------------
Tests run: 41, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.514 s -- in com.ankurm.synchronizers.RoundOrderingCorrectnessTest
+43
View File
@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.ankurm</groupId>
<artifactId>java-core-examples</artifactId>
<version>1.0</version>
</parent>
<artifactId>synchronizers</artifactId>
<name>synchronizers</name>
<description>CountDownLatch vs CyclicBarrier vs Phaser vs Semaphore: one three-round worker pipeline implemented four ways, reuse semantics, timeout behavior, and virtual-thread compatibility.</description>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.11.0</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<release>25</release>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.5</version>
</plugin>
</plugins>
</build>
</project>
+69
View File
@@ -0,0 +1,69 @@
#!/usr/bin/env bash
# Regenerates every file in output/. Requires:
# - JDK25_HOME pointing at a JDK 24+ install (this repo used Temurin 25.0.4.1+1)
# - JDK21_HOME pointing at a pre-JDK-24 install (this repo used OpenJDK 21.0.10), for the
# virtual-thread pinning contrast at the end
set -euo pipefail
cd "$(dirname "$0")/.."
: "${JDK25_HOME:?Set JDK25_HOME to a JDK 24+ install}"
: "${JDK21_HOME:?Set JDK21_HOME to a pre-JDK-24 install}"
mkdir -p target/classes target/classes-21
"$JDK25_HOME/bin/javac" --release 25 -d target/classes $(find src/main/java -name "*.java")
"$JDK21_HOME/bin/javac" --release 21 -d target/classes-21 $(find src/main/java -name "*.java")
echo "--- 01: CountDownLatch pipeline ---"
{
echo '$ java CountDownLatchPipeline'
echo ""
"$JDK25_HOME/bin/java" -cp target/classes com.ankurm.synchronizers.CountDownLatchPipeline 2>&1 | grep -v "Picked up"
} > output/01-countdownlatch-pipeline.txt
echo "--- 02: CyclicBarrier pipeline ---"
{
echo '$ java CyclicBarrierPipeline'
echo ""
"$JDK25_HOME/bin/java" -cp target/classes com.ankurm.synchronizers.CyclicBarrierPipeline 2>&1 | grep -v "Picked up"
} > output/02-cyclicbarrier-pipeline.txt
echo "--- 03: Phaser pipeline (dynamic registration) ---"
{
echo '$ java PhaserPipeline'
echo ""
"$JDK25_HOME/bin/java" -cp target/classes com.ankurm.synchronizers.PhaserPipeline 2>&1 | grep -v "Picked up"
} > output/03-phaser-pipeline.txt
echo "--- 04: Semaphore resource guard ---"
{
echo '$ java SemaphoreResourceGuard'
echo ""
"$JDK25_HOME/bin/java" -cp target/classes com.ankurm.synchronizers.SemaphoreResourceGuard 2>&1 | grep -v "Picked up"
} > output/04-semaphore-resource-guard.txt
echo "--- 05: timeout behavior across all four ---"
{
echo '$ java TimeoutBehaviorDemo'
echo ""
"$JDK25_HOME/bin/java" -cp target/classes com.ankurm.synchronizers.TimeoutBehaviorDemo 2>&1 | grep -v "Picked up"
} > output/05-timeout-behavior.txt
echo "--- 06: virtual-thread compatibility, all four, on JDK21 (pre-JEP-491) ---"
{
echo '$ java -Djdk.tracePinnedThreads=full VirtualThreadCompatibilityDemo (pre-JDK-24 runtime)'
echo ""
"$JDK21_HOME/bin/java" -Djdk.tracePinnedThreads=full -cp target/classes-21 \
com.ankurm.synchronizers.VirtualThreadCompatibilityDemo 2>&1 | grep -v "Picked up"
echo ""
echo '$ java -Djdk.tracePinnedThreads=full SynchronizedWaitPinsDemo (pre-JDK-24 runtime, contrast case)'
echo ""
"$JDK21_HOME/bin/java" -Djdk.tracePinnedThreads=full -cp target/classes-21 \
com.ankurm.synchronizers.SynchronizedWaitPinsDemo 2>&1 | grep -v "Picked up"
} > output/06-virtual-thread-compatibility.txt
echo "--- 07: correctness tests ---"
"$JDK25_HOME/bin/java" --version > /dev/null # sanity
(cd "$(pwd)" && JAVA_HOME="$JDK25_HOME" mvn -q -f pom.xml test 2>&1 | grep -v "Picked up\|WARNING" || true)
cp target/surefire-reports/com.ankurm.synchronizers.RoundOrderingCorrectnessTest.txt output/07-round-ordering-correctness.txt
echo "Done. See output/."
@@ -0,0 +1,70 @@
package com.ankurm.synchronizers;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeUnit;
/**
* The same three-round worker pipeline every class in this module runs, done with
* {@link CountDownLatch}. Two uses, both idiomatic:
*
* <ol>
* <li>A single "starting gate" latch releases all workers at once instead of letting them
* race to start as soon as each is spawned.</li>
* <li>One NEW latch per round enforces "everyone finishes round N before round N+1 starts" -
* because a {@code CountDownLatch} counts down to zero exactly once and cannot be reset.
* That's the limitation {@link CyclicBarrierPipeline} exists to remove.</li>
* </ol>
*/
public final class CountDownLatchPipeline {
private static final int WORKERS = 4;
private static final int ROUNDS = 3;
public static void main(String[] args) throws Exception {
List<String> log = new CopyOnWriteArrayList<>();
CountDownLatch startGate = new CountDownLatch(1);
Thread[] workers = new Thread[WORKERS];
for (int w = 0; w < WORKERS; w++) {
int id = w;
workers[w] = new Thread(() -> runWorker(id, startGate, log));
workers[w].start();
}
log.add("main: all " + WORKERS + " workers created, still waiting at the start gate");
Thread.sleep(50); // let them actually reach await() before we release them
log.add("main: releasing the start gate");
startGate.countDown();
for (Thread t : workers) {
t.join();
}
log.forEach(System.out::println);
System.out.println("done");
}
private static void runWorker(int id, CountDownLatch startGate, List<String> log) {
try {
startGate.await();
CountDownLatch roundLatch = null;
for (int round = 1; round <= ROUNDS; round++) {
// A fresh latch every round - CountDownLatch cannot be reused once it hits zero.
roundLatch = ROUND_LATCHES[round - 1];
Thread.sleep(20 + (id * 5)); // simulate uneven round work
log.add("worker-" + id + ": finished round " + round);
roundLatch.countDown();
roundLatch.await(5, TimeUnit.SECONDS);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
// One latch per round, sized to the worker count, created up front so every worker sees the
// same instances. In real code this bookkeeping is exactly what CyclicBarrier automates.
private static final CountDownLatch[] ROUND_LATCHES = {
new CountDownLatch(WORKERS), new CountDownLatch(WORKERS), new CountDownLatch(WORKERS)
};
}
@@ -0,0 +1,54 @@
package com.ankurm.synchronizers;
import java.util.List;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CyclicBarrier;
/**
* The same three-round pipeline as {@link CountDownLatchPipeline}, but with the one thing a
* {@code CountDownLatch} can't do: the barrier resets itself automatically after every round, so
* one {@code CyclicBarrier} instance - not one per round - carries the whole pipeline. It also
* runs an optional barrier action exactly once per round, on whichever thread happens to be the
* last to arrive, useful for round-boundary bookkeeping that shouldn't run once per worker.
*/
public final class CyclicBarrierPipeline {
private static final int WORKERS = 4;
private static final int ROUNDS = 3;
public static void main(String[] args) throws Exception {
List<String> log = new CopyOnWriteArrayList<>();
int[] roundCounter = {0};
CyclicBarrier barrier = new CyclicBarrier(WORKERS, () -> {
roundCounter[0]++;
log.add("barrier action: round " + roundCounter[0] + " complete, all " + WORKERS + " workers arrived");
});
Thread[] workers = new Thread[WORKERS];
for (int w = 0; w < WORKERS; w++) {
int id = w;
workers[w] = new Thread(() -> runWorker(id, barrier, log));
workers[w].start();
}
for (Thread t : workers) {
t.join();
}
log.forEach(System.out::println);
System.out.println("done");
}
private static void runWorker(int id, CyclicBarrier barrier, List<String> log) {
try {
for (int round = 1; round <= ROUNDS; round++) {
Thread.sleep(20 + (id * 5));
log.add("worker-" + id + ": finished round " + round);
barrier.await(); // same barrier object, every round - it resets on its own
}
} catch (InterruptedException | BrokenBarrierException e) {
Thread.currentThread().interrupt();
}
}
}
@@ -0,0 +1,68 @@
package com.ankurm.synchronizers;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Phaser;
/**
* The same pipeline again, with {@link Phaser} - which does everything
* {@link CyclicBarrierPipeline} does (a reusable, self-resetting barrier with a per-phase
* action, via {@link Phaser#onAdvance}), plus the one thing neither {@code CountDownLatch} nor
* {@code CyclicBarrier} can: a party count fixed at construction time. Here, a fifth worker
* {@link Phaser#register()}s itself only after round 1 has already completed, and the barrier
* for round 2 correctly waits for all five - something that requires building a brand new
* {@code CyclicBarrier} (and re-pointing every existing worker at it) to do with that class.
*/
public final class PhaserPipeline {
private static final int INITIAL_WORKERS = 4;
private static final int ROUNDS = 3;
public static void main(String[] args) throws Exception {
List<String> log = new CopyOnWriteArrayList<>();
Phaser phaser = new Phaser(INITIAL_WORKERS) {
@Override
protected boolean onAdvance(int phase, int registeredParties) {
log.add("phaser onAdvance: phase " + (phase + 1) + " complete, "
+ registeredParties + " registered parties -> advancing");
return phase + 1 >= ROUNDS || registeredParties == 0;
}
};
Thread[] workers = new Thread[INITIAL_WORKERS];
for (int w = 0; w < INITIAL_WORKERS; w++) {
int id = w;
workers[w] = new Thread(() -> runWorker(id, phaser, log, 1));
workers[w].start();
}
for (Thread t : workers) {
t.join();
}
log.forEach(System.out::println);
System.out.println("done");
}
private static void runWorker(int id, Phaser phaser, List<String> log, int startRound) {
try {
for (int round = startRound; round <= ROUNDS; round++) {
Thread.sleep(20 + (id * 5));
log.add("worker-" + id + ": finished round " + round);
phaser.arriveAndAwaitAdvance();
// The late-joining worker: registers itself right after round 1 finishes, then
// runs rounds 2 and 3 like everyone else. Nobody else needed to change.
if (id == 0 && round == 1) {
int lateId = 4;
phaser.register();
log.add("worker-" + lateId + ": registers after round 1 (phaser now has "
+ phaser.getRegisteredParties() + " parties)");
new Thread(() -> runWorker(lateId, phaser, log, 2)).start();
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
@@ -0,0 +1,74 @@
package com.ankurm.synchronizers;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicInteger;
/**
* {@link Semaphore} solves a genuinely different problem than the other three classes in this
* module, even though it gets listed alongside them constantly. {@code CountDownLatch},
* {@code CyclicBarrier}, and {@code Phaser} all answer "has everyone reached this point yet?" -
* {@code Semaphore} answers "how many callers are allowed through this point at once?" Nothing
* here stops multiple threads from acquiring at wildly different times; it just caps how many
* hold a permit simultaneously.
*
* <p>Same three-round pipeline shape as the other demos, but this time each worker also needs a
* permit from a 2-permit {@code Semaphore} before it can call a simulated expensive shared
* resource (a rate-limited downstream API, a connection pool) once per round - independent of
* which round the barrier logic is in, and independent of worker identity.
*/
public final class SemaphoreResourceGuard {
private static final int WORKERS = 4;
private static final int ROUNDS = 3;
private static final int PERMITS = 2;
public static void main(String[] args) throws Exception {
List<String> log = new CopyOnWriteArrayList<>();
Semaphore resourceGuard = new Semaphore(PERMITS, true); // fair, for a predictable transcript
AtomicInteger concurrentInResource = new AtomicInteger();
AtomicInteger maxObservedConcurrency = new AtomicInteger();
CyclicBarrier roundBarrier = new CyclicBarrier(WORKERS);
Thread[] workers = new Thread[WORKERS];
for (int w = 0; w < WORKERS; w++) {
int id = w;
workers[w] = new Thread(() -> runWorker(
id, resourceGuard, roundBarrier, concurrentInResource, maxObservedConcurrency, log));
workers[w].start();
}
for (Thread t : workers) {
t.join();
}
log.forEach(System.out::println);
System.out.println("permits=" + PERMITS + " maxObservedConcurrency=" + maxObservedConcurrency.get());
System.out.println("done");
}
private static void runWorker(int id, Semaphore resourceGuard, CyclicBarrier roundBarrier,
AtomicInteger concurrentInResource, AtomicInteger maxObservedConcurrency,
List<String> log) {
try {
for (int round = 1; round <= ROUNDS; round++) {
resourceGuard.acquire();
try {
int inFlight = concurrentInResource.incrementAndGet();
maxObservedConcurrency.updateAndGet(max -> Math.max(max, inFlight));
log.add("worker-" + id + " round " + round + ": acquired permit, "
+ inFlight + " callers concurrently in the resource"
+ " (availablePermits=" + resourceGuard.availablePermits() + ")");
Thread.sleep(15);
} finally {
concurrentInResource.decrementAndGet();
resourceGuard.release();
}
roundBarrier.await(); // unrelated barrier concern, same as the other demos
}
} catch (Exception e) {
Thread.currentThread().interrupt();
}
}
}
@@ -0,0 +1,59 @@
package com.ankurm.synchronizers;
/**
* The contrast case for {@link VirtualThreadCompatibilityDemo} - and a correction to a common
* assumption along the way. The naive expectation is that hand-rolling "wait for a signal" with
* {@code synchronized} + {@code Object.wait()}/{@code notifyAll()} instead of a
* {@code java.util.concurrent} class would pin, the same way {@code synchronized} + a blocking
* call pinned before JEP 491. It doesn't: {@code Object.wait()} fully releases the monitor for
* the duration of the wait, so there's no lock held while parked, and nothing to pin. What
* genuinely pins on a pre-JDK-24 runtime is a blocking call made <em>while still holding</em> the
* monitor - {@code Thread.sleep()} inside {@code synchronized}, exactly like
* {@code MonitorPinningDemo} in the {@code vt-pinning} module. Both cases run here, back to back,
* under the same trace flag, so the difference is visible in one transcript instead of asserted.
*/
public final class SynchronizedWaitPinsDemo {
private static final Object LOCK = new Object();
private static boolean signalled = false;
public static void main(String[] args) throws Exception {
System.out.println("java.version=" + System.getProperty("java.version"));
System.out.println("jdk.tracePinnedThreads=" + System.getProperty("jdk.tracePinnedThreads"));
// Case 1: synchronized + Object.wait() - releases the monitor while parked, does not pin.
Thread waitThread = Thread.ofVirtual().start(() -> {
synchronized (LOCK) {
try {
while (!signalled) {
LOCK.wait();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
});
Thread.sleep(100);
synchronized (LOCK) {
signalled = true;
LOCK.notifyAll();
}
waitThread.join();
System.out.println("synchronized + Object.wait(): completed, no pinning trace above this line for it");
// Case 2: synchronized + Thread.sleep() - still holds the monitor while parked, DOES pin.
Thread sleepThread = Thread.ofVirtual().start(() -> {
synchronized (LOCK) {
try {
Thread.sleep(150);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
});
sleepThread.join();
System.out.println("synchronized + Thread.sleep(): completed, pinning trace (if any) appears above this line for it");
System.out.println("done");
}
}
@@ -0,0 +1,89 @@
package com.ankurm.synchronizers;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.Phaser;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
/**
* What each class does when the thing it's waiting for never happens, deliberately forced by
* under-supplying arrivals or permits. The four don't behave identically, which matters more
* than it looks: a timed-out {@code CyclicBarrier} poisons itself for every other waiter, and
* the other three don't.
*/
public final class TimeoutBehaviorDemo {
public static void main(String[] args) throws Exception {
countDownLatchTimeout();
cyclicBarrierTimeout();
phaserTimeout();
semaphoreTimeout();
System.out.println("done");
}
private static void countDownLatchTimeout() throws InterruptedException {
System.out.println("=== CountDownLatch.await(timeout): expects 2, only 1 ever counts down ===");
CountDownLatch latch = new CountDownLatch(2);
latch.countDown(); // only one of two - the second never arrives
boolean released = latch.await(200, TimeUnit.MILLISECONDS);
System.out.println("await returned: " + released + " (false = timed out, latch still usable to re-check later)");
System.out.println("getCount() after timeout: " + latch.getCount());
}
private static void cyclicBarrierTimeout() throws InterruptedException {
System.out.println();
System.out.println("=== CyclicBarrier.await(timeout): expects 2 parties, only 1 arrives ===");
CyclicBarrier barrier = new CyclicBarrier(2);
try {
barrier.await(200, TimeUnit.MILLISECONDS);
} catch (TimeoutException e) {
System.out.println("await threw TimeoutException, as expected");
System.out.println("barrier.isBroken() after the timeout: " + barrier.isBroken());
} catch (BrokenBarrierException e) {
System.out.println("unexpected BrokenBarrierException");
}
// A second, unrelated thread trying to use the SAME barrier instance afterward:
try {
barrier.await(50, TimeUnit.MILLISECONDS);
} catch (BrokenBarrierException e) {
System.out.println("a second, later await() on the SAME barrier: BrokenBarrierException immediately"
+ " - one timeout poisons the barrier for everyone until reset()");
} catch (TimeoutException e) {
System.out.println("unexpected TimeoutException on the second await");
}
}
private static void phaserTimeout() {
System.out.println();
System.out.println("=== Phaser.awaitAdvanceInterruptibly(phase, timeout): expects 2 parties, only 1 arrives ===");
Phaser phaser = new Phaser(2);
int phase = phaser.arrive(); // this party arrives; the second one never does
try {
phaser.awaitAdvanceInterruptibly(phase, 200, TimeUnit.MILLISECONDS);
} catch (TimeoutException e) {
System.out.println("awaitAdvanceInterruptibly threw TimeoutException, as expected");
System.out.println("phaser.getPhase() after the timeout: " + phaser.getPhase()
+ " (unchanged - still phase " + phase + ", not advanced or broken)");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
// A later, correctly-completed round on the SAME phaser instance, no reset needed:
phaser.arriveAndAwaitAdvance();
System.out.println("a later round on the SAME phaser, once both parties actually arrive: "
+ "succeeds normally, now at phase " + phaser.getPhase());
}
private static void semaphoreTimeout() throws InterruptedException {
System.out.println();
System.out.println("=== Semaphore.tryAcquire(timeout): 0 permits available ===");
Semaphore semaphore = new Semaphore(0);
boolean acquired = semaphore.tryAcquire(200, TimeUnit.MILLISECONDS);
System.out.println("tryAcquire returned: " + acquired + " (false = timed out, no exception, no broken state)");
semaphore.release();
boolean acquiredAfterRelease = semaphore.tryAcquire(50, TimeUnit.MILLISECONDS);
System.out.println("a later tryAcquire after a release: " + acquiredAfterRelease + " - fully independent of the earlier timeout");
}
}
@@ -0,0 +1,96 @@
package com.ankurm.synchronizers;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.Phaser;
import java.util.concurrent.Semaphore;
/**
* All four classes in this module are built on {@link java.util.concurrent.locks.AbstractQueuedSynchronizer}
* (Phaser uses its own compare-and-swap state machine, not AQS directly, but the same
* park/unpark mechanism), not on {@code synchronized}. A virtual thread blocking inside any of
* them parks via {@code LockSupport}, which was always safe to unmount from - none of the four
* ever pinned a carrier, on any JDK version, including JDK 21 well before
* <a href="https://openjdk.org/jeps/491">JEP 491</a> fixed {@code synchronized}. This demo blocks
* a virtual thread inside each one in turn, run with {@code -Djdk.tracePinnedThreads=full}: if
* it printed anything, that would be news. It doesn't. Compare against
* {@link SynchronizedWaitPinsDemo}, the one construct in this module family that genuinely does
* pin on a pre-JDK-24 runtime.
*/
public final class VirtualThreadCompatibilityDemo {
public static void main(String[] args) throws Exception {
System.out.println("java.version=" + System.getProperty("java.version"));
System.out.println("jdk.tracePinnedThreads=" + System.getProperty("jdk.tracePinnedThreads"));
runOnVirtualThread("CountDownLatch.await()", () -> {
CountDownLatch latch = new CountDownLatch(1);
Thread.ofPlatform().start(() -> {
sleepQuietly(100);
latch.countDown();
});
latch.await();
});
runOnVirtualThread("CyclicBarrier.await()", () -> {
CyclicBarrier barrier = new CyclicBarrier(2);
Thread.ofPlatform().start(() -> {
sleepQuietly(100);
awaitQuietly(barrier);
});
barrier.await();
});
runOnVirtualThread("Phaser.arriveAndAwaitAdvance()", () -> {
Phaser phaser = new Phaser(2);
Thread.ofPlatform().start(() -> {
sleepQuietly(100);
phaser.arriveAndAwaitAdvance();
});
phaser.arriveAndAwaitAdvance();
});
runOnVirtualThread("Semaphore.acquire()", () -> {
Semaphore semaphore = new Semaphore(0);
Thread.ofPlatform().start(() -> {
sleepQuietly(100);
semaphore.release();
});
semaphore.acquire();
});
System.out.println("done - no pinning trace printed above for any of the four");
}
private interface BlockingAction {
void run() throws Exception;
}
private static void runOnVirtualThread(String label, BlockingAction action) throws InterruptedException {
Thread t = Thread.ofVirtual().start(() -> {
try {
action.run();
} catch (Exception e) {
throw new RuntimeException(e);
}
});
t.join();
System.out.println(label + ": completed, no trace above this line for it");
}
private static void sleepQuietly(long ms) {
try {
Thread.sleep(ms);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
private static void awaitQuietly(CyclicBarrier barrier) {
try {
barrier.await();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
@@ -0,0 +1,137 @@
package com.ankurm.synchronizers;
import org.junit.jupiter.api.RepeatedTest;
import org.junit.jupiter.api.Test;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.Phaser;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* Sanity checks, not throughput or pinning proofs - see the module README and the article's
* own quoted transcripts for those. These assert the one property that actually matters for a
* round barrier: no worker's round-N+1 work is ever observed to start before every worker's
* round-N work has finished, repeated to catch a scheduling-dependent race a single run wouldn't.
*/
class RoundOrderingCorrectnessTest {
private static final int WORKERS = 6;
private static final int ROUNDS = 4;
@RepeatedTest(20)
void cyclicBarrierNeverStartsARoundEarly() throws Exception {
AtomicInteger currentRound = new AtomicInteger(1);
AtomicInteger[] finishedThisRound = newCounters();
CyclicBarrier barrier = new CyclicBarrier(WORKERS, () -> currentRound.incrementAndGet());
Thread[] workers = new Thread[WORKERS];
for (int w = 0; w < WORKERS; w++) {
workers[w] = new Thread(() -> {
for (int round = 1; round <= ROUNDS; round++) {
// The assertion: this worker's round MUST equal currentRound at the moment
// it starts working - if some other worker's barrier.await() returned before
// this one arrived, currentRound would already have advanced past `round`.
assertEquals(round, currentRound.get(),
"worker started round " + round + " but currentRound was already " + currentRound.get());
finishedThisRound[round - 1].incrementAndGet();
try {
barrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
throw new RuntimeException(e);
}
}
});
workers[w].start();
}
for (Thread t : workers) {
t.join();
}
for (AtomicInteger count : finishedThisRound) {
assertEquals(WORKERS, count.get(), "every worker must have finished every round exactly once");
}
}
@RepeatedTest(20)
void phaserNeverStartsARoundEarly() {
AtomicInteger currentRound = new AtomicInteger(1);
AtomicInteger[] finishedThisRound = newCounters();
Phaser phaser = new Phaser(WORKERS) {
@Override
protected boolean onAdvance(int phase, int registeredParties) {
currentRound.incrementAndGet();
return phase + 1 >= ROUNDS || registeredParties == 0;
}
};
Thread[] workers = new Thread[WORKERS];
for (int w = 0; w < WORKERS; w++) {
workers[w] = new Thread(() -> {
for (int round = 1; round <= ROUNDS; round++) {
assertEquals(round, currentRound.get(),
"worker started round " + round + " but currentRound was already " + currentRound.get());
finishedThisRound[round - 1].incrementAndGet();
phaser.arriveAndAwaitAdvance();
}
});
workers[w].start();
}
for (Thread t : workers) {
try {
t.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
for (AtomicInteger count : finishedThisRound) {
assertEquals(WORKERS, count.get(), "every worker must have finished every round exactly once");
}
}
@Test
void semaphoreNeverExceedsItsPermitCount() throws Exception {
int permits = 3;
int callers = 30;
Semaphore semaphore = new Semaphore(permits);
AtomicInteger concurrent = new AtomicInteger();
AtomicInteger maxObserved = new AtomicInteger();
Thread[] threads = new Thread[callers];
for (int i = 0; i < callers; i++) {
threads[i] = new Thread(() -> {
try {
semaphore.acquire();
try {
int inFlight = concurrent.incrementAndGet();
maxObserved.updateAndGet(max -> Math.max(max, inFlight));
Thread.sleep(5);
} finally {
concurrent.decrementAndGet();
semaphore.release();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
threads[i].start();
}
for (Thread t : threads) {
t.join();
}
assertTrue(maxObserved.get() <= permits,
"observed " + maxObserved.get() + " concurrent callers with only " + permits + " permits");
assertEquals(permits, semaphore.availablePermits(), "all permits must be returned when every caller finishes");
}
private static AtomicInteger[] newCounters() {
AtomicInteger[] counters = new AtomicInteger[ROUNDS];
for (int i = 0; i < ROUNDS; i++) {
counters[i] = new AtomicInteger();
}
return counters;
}
}