⚡ Advanced Concurrency · Advanced

Synchronizers in Java

CountDownLatch, CyclicBarrier, Semaphore, Phaser.

🧩 The mysteryA launch waits for 3 systems to report ready. Relay runners regroup after every lap. A club lets only 100 people inside. Java has a ready-made tool for each of these.

CountDownLatch: wait for N events

A latch starts at a count. Each countDown() lowers it; await() blocks until it reaches zero. It's one-shot: once open, it stays open and can't be reset.

var ready = new CountDownLatch(3);
// each subsystem, when ready:
ready.countDown();
// launch control:
ready.await();   // waits for all 3
🔮 Predict it

Your turn

Four threads each add their number, then count down. What prints?

var latch = new CountDownLatch(4);
var sum = new AtomicInteger();
for (int v : new int[] {1, 2, 3, 4})
    Thread.ofPlatform().start(() -> {
        sum.addAndGet(v);
        latch.countDown();
    });
latch.await();
System.out.println(sum.get());
  1. 10
  2. 0
  3. A value from 0 to 10
Show the answer

Always 10. await() returns only after all four countDown() calls, and each thread adds its number *before* counting down. So main sees 1 + 2 + 3 + 4.

CyclicBarrier: meet, then go

A barrier makes a fixed group of N threads wait for each other at await(). When the last one arrives, all proceed together, and the barrier resets for the next round. Think relay runners regrouping after every lap.

Semaphore: N permits

A semaphore holds N permits. acquire() takes one, waiting if none are left; release() gives one back; tryAcquire() never waits, it just returns false. It caps how many threads can do something at once.

var sem = new Semaphore(10);
sem.acquire();
try {
    callPartnerApi();
} finally {
    sem.release();
}
🔮 Predict it

Count the permits

What does this print?

var sem = new Semaphore(3);
sem.acquire();
sem.acquire();
System.out.println(sem.availablePermits());
System.out.println(sem.tryAcquire(2));
System.out.println(sem.tryAcquire());
  1. 1 false true
  2. 1 true false
  3. 3 true true
Show the answer

Two acquire() calls leave 1 permit. tryAcquire(2) asks for two, so it fails at once without blocking. tryAcquire() asks for one and gets the last permit: true.

⚠️ The trap

A latch is not a barrier

Need repeated rounds? A CountDownLatch can't be reset: once at zero, it stays open. Use CyclicBarrier for a fixed group meeting again and again, or Phaser: a flexible, reusable barrier whose number of parties can change between phases.

💼 In the real world

Picking the tool

A partner API allows 10 concurrent calls and you have 200 request threads: Semaphore(10). Integration tests wait for async callbacks with a CountDownLatch. Simulations that advance in steps use CyclicBarrier or Phaser. All tested and expressive: reach for them before hand-rolling wait/notify.

Key takeaways

  1. CountDownLatch: one-shot; countDown() and await()
  2. CyclicBarrier: N threads meet, then it resets
  3. Semaphore: acquire()/release() caps concurrency at N
  4. Phaser: dynamic parties across multiple phases
🤯 Did you know?

Semaphore permits aren't owned by threads: one thread can release a permit another acquired, and extra release() calls can push the count above its starting value.

Practice questions

What does this print?

var latch = new CountDownLatch(3);
var done = new AtomicInteger();
for (int i = 0; i < 3; i++) {
    Thread.ofPlatform().start(() -> {
        done.incrementAndGet();
        latch.countDown();
    });
}
latch.await();
System.out.println(done.get());
  1. 3
  2. 0
  3. A value from 0 to 3
  4. 1
Check your answer

3. await() returns only after all three countDown() calls, and each happens after that thread's increment. So main always sees 3.

What does this print?

var sem = new Semaphore(2);
System.out.println(sem.tryAcquire());
System.out.println(sem.tryAcquire());
System.out.println(sem.tryAcquire());
sem.release();
System.out.println(sem.availablePermits());
  1. true true false 1
  2. true true true 0
  3. true false false 2
  4. true true false 2
Check your answer

true true false 1. Two permits allow two acquisitions; the third tryAcquire() fails without blocking. Releasing one permit makes 1 available again.

Next: split a giant job in half, then in half again, and let idle threads steal the pieces. The Fork/Join framework.