Meeting Points — Latch, Barrier, Semaphore

QuickCart does not run alone. When a customer checks out, Priya’s app must talk to inventory, payments, and shipping. Sometimes one thread must wait until N jobs finish. Sometimes N threads must all reach a line, then go together. Sometimes only a fixed number of threads may use the database at once. Java gives three small tools for these patterns.

CountDownLatch — wait for N services

A CountDownLatch starts with a number. Each finished task calls countDown(). Threads that call await() block until the count hits zero.

Priya uses this at checkout. QuickCart must hear back from inventory, payment, and shipping before telling the customer “Order confirmed”:

import java.util.concurrent.CountDownLatch;

public void confirmOrder(String orderId) throws InterruptedException {
    CountDownLatch latch = new CountDownLatch(3);

    new Thread(() -> {
        try { inventory.reserve(orderId); }
        finally { latch.countDown(); }
    }).start();

    new Thread(() -> {
        try { payment.charge(orderId); }
        finally { latch.countDown(); }
    }).start();

    new Thread(() -> {
        try { shipping.schedule(orderId); }
        finally { latch.countDown(); }
    }).start();

    latch.await();
    System.out.println("Order " + orderId + " confirmed");
}

Always call countDown() in a finally block. If a service fails and you skip countDown(), everyone waits forever.

Key methods

CountDownLatch latch = new CountDownLatch(5);

latch.countDown();              // Decrement count (non-blocking)
latch.await();                  // Block until count reaches 0
boolean done = latch.await(5, TimeUnit.SECONDS); // Wait with timeout
int remaining = latch.getCount(); // How many countDowns still needed

Full example with executor

Priya processes a batch of orders and waits for all to finish:

public class BatchOrderProcessor {
    private final ExecutorService executor = Executors.newFixedThreadPool(4);

    public void processOrders(List<String> orderIds) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(orderIds.size());

        for (String orderId : orderIds) {
            executor.submit(() -> {
                try {
                    processOrder(orderId);
                    System.out.println("Completed: " + orderId);
                } finally {
                    latch.countDown();
                }
            });
        }

        latch.await(); // Wait for all orders
        System.out.println("All orders processed");
    }

    private void processOrder(String orderId) {
        // Validate, charge, ship
    }
}

Characteristics:

  • One-time use — you cannot reset the count. When it hits zero, that latch is done.
  • Count can only decrease, never go back up.
  • Multiple threads can call await() — they all unblock when count reaches zero.
  • countDown() never blocks.

If you need to coordinate again, create a new CountDownLatch.

CyclicBarrier — all wait, then all go

A CyclicBarrier makes N threads wait at a gate. When the last one arrives, the gate opens and everyone continues. The barrier can be reused for the next round.

Think of a relay race. Runners must all reach the handoff line before the next leg starts.

import java.util.concurrent.CyclicBarrier;

int workers = 3;
CyclicBarrier barrier = new CyclicBarrier(workers, () -> {
    System.out.println("All workers arrived — next stage begins");
});

for (int i = 1; i <= workers; i++) {
    int id = i;
    new Thread(() -> {
        try {
            System.out.println("Worker " + id + " finished stage 1");
            barrier.await(); // Wait until all 3 arrive
            System.out.println("Worker " + id + " starting stage 2");
        } catch (Exception e) {
            Thread.currentThread().interrupt();
        }
    }).start();
}

The optional runnable runs once when all threads reach the barrier — useful for logging or kicking off the next phase.

Key methods

CyclicBarrier barrier = new CyclicBarrier(3);

barrier.await();                              // Wait at barrier
barrier.await(5, TimeUnit.SECONDS);           // Wait with timeout
barrier.reset();                              // Reset for reuse
int parties = barrier.getParties();           // Number of threads required
int waiting = barrier.getNumberWaiting();     // How many are waiting now

Multi-stage price update

Priya runs a nightly price update. Every product must finish step one before any product starts step two:

public class PriceUpdateJob {
    public void updatePrices(List<String> products) {
        CyclicBarrier barrier = new CyclicBarrier(products.size(), () -> {
            System.out.println("Stage 1 done — applying discounts");
        });

        for (String product : products) {
            new Thread(() -> {
                try {
                    fetchCurrentPrice(product);
                    barrier.await();  // Wait for all products to finish step 1
                    applyDiscount(product);
                    barrier.await();  // Wait for all to finish step 2
                    publishPrice(product);
                } catch (Exception e) {
                    Thread.currentThread().interrupt();
                }
            }).start();
        }
    }

    private void fetchCurrentPrice(String product) { /* ... */ }
    private void applyDiscount(String product) { /* ... */ }
    private void publishPrice(String product) { /* ... */ }
}

The barrier resets automatically after all parties arrive. Same barrier object works for stage 2.

If one thread never reaches await(), the others wait forever. Handle errors carefully. If a thread fails at the barrier, others may get BrokenBarrierException.

Semaphore — limit concurrent DB calls

A Semaphore holds a fixed number of permits. To enter a protected area, a thread acquire()s a permit. When done, it release()s the permit. If no permits are left, acquire() waits.

QuickCart’s database accepts only five connections at a time. A semaphore with five permits enforces that:

import java.util.concurrent.Semaphore;

Semaphore dbLimit = new Semaphore(5);

public void queryDatabase(String sql) throws InterruptedException {
    dbLimit.acquire(); // Wait if all 5 permits are taken
    try {
        runQuery(sql);
    } finally {
        dbLimit.release(); // Always give the permit back
    }
}

Without this cap, a traffic spike could open hundreds of connections and crash the database.

Key methods

Semaphore semaphore = new Semaphore(3);

semaphore.acquire();                          // Block until permit available
semaphore.acquire(2);                         // Acquire 2 permits at once
boolean got = semaphore.tryAcquire();         // Non-blocking — false if none
boolean got2 = semaphore.tryAcquire(5, TimeUnit.SECONDS); // Wait up to 5 sec
semaphore.release();                          // Return one permit
semaphore.release(2);                         // Return two permits
int available = semaphore.availablePermits(); // Permits free right now

Fair vs non-fair

By default, semaphores are non-fair — better performance, but a thread that just released a permit might acquire again before a thread that waited longer.

// Non-fair (default) — faster
Semaphore fast = new Semaphore(5, false);

// Fair — threads acquire in roughly FIFO order
Semaphore fair = new Semaphore(5, true);

Use fair semaphores when starvation matters — every waiting thread should eventually get a turn. Use non-fair when raw speed matters more.

Connection pool pattern

Priya models a simple connection pool with a semaphore and a queue:

import java.util.concurrent.ConcurrentLinkedQueue;

public class QuickCartConnectionPool {
    private final Semaphore semaphore;
    private final Queue<Connection> connections;

    public QuickCartConnectionPool(int maxConnections) {
        this.semaphore = new Semaphore(maxConnections);
        this.connections = new ConcurrentLinkedQueue<>();
        for (int i = 0; i < maxConnections; i++) {
            connections.offer(createConnection());
        }
    }

    public Connection acquire() throws InterruptedException {
        semaphore.acquire();
        return connections.poll();
    }

    public void release(Connection connection) {
        connections.offer(connection);
        semaphore.release();
    }

    private Connection createConnection() {
        return new Connection(); // placeholder
    }
}

Rate limiting API calls

QuickCart calls an external shipping API with a limit of ten concurrent requests:

public class ShippingApiClient {
    private final Semaphore rateLimit = new Semaphore(10);

    public void scheduleShipment(String orderId) throws InterruptedException {
        rateLimit.acquire();
        try {
            callShippingApi(orderId);
        } finally {
            rateLimit.release();
        }
    }

    private void callShippingApi(String orderId) {
        // HTTP call
    }
}

Use tryAcquire() when you would rather fail fast than wait:

if (!dbLimit.tryAcquire()) {
    throw new RuntimeException("Database busy — try again");
}
try {
    runQuery(sql);
} finally {
    dbLimit.release();
}

Three tools, three jobs

Tool Question it answers Reusable?
CountDownLatch “Are all N tasks done yet?” No
CyclicBarrier “Has every thread reached this point?” Yes
CyclicBarrier Multi-stage: all finish stage 1, then all start stage 2 Yes
Semaphore “How many threads may enter at once?” Yes
Feature CountDownLatch CyclicBarrier Semaphore
Purpose Wait for completion Wait for each other Control access
Count direction Decrease only Resets after all arrive Increase and decrease
Multiple awaits Yes Yes Yes (acquire)

When to pick each:

  • CountDownLatch — one thread waits for N workers to finish (checkout confirmation, batch job done).
  • CyclicBarrier — N threads sync between stages (multi-step batch jobs, phased processing).
  • Semaphore — cap access to a scarce resource (DB connections, API rate limits).

Handling InterruptedException

All three can throw InterruptedException when waiting. Restore the interrupt flag so callers know the thread was interrupted:

try {
    latch.await();
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();
    // Clean up and exit
}

What not to do

Do not reuse a CountDownLatch whose count already reached zero — await() returns immediately:

// BAD: Count already at 0
latch.await(); // Won't wait — latch is spent

Do not forget countDown() or release() — that causes deadlock:

// BAD: Main thread waits forever
latch.await();
// Missing: latch.countDown() in worker

Do not acquire a semaphore without releasing in finally:

// BAD: Permit never returned
semaphore.acquire();
runQuery(sql);
// Missing: semaphore.release()

What to remember

  • CountDownLatch counts down to zero; await() unblocks when done.
  • Put countDown() in finally so failures do not cause deadlock.
  • A latch is one-time use — create a new one for the next batch.
  • CyclicBarrier syncs threads at a gate and can run again for the next round.
  • Semaphore limits how many threads use a resource at the same time.
  • Fair semaphores (new Semaphore(n, true)) reduce starvation; non-fair is faster.
  • Always release() permits and handle InterruptedException properly.

What Priya does next: she has thousands of products to scan for a sale badge. She tries parallelStream() to spread the work across CPU cores.