Chaturmind
LearnDSASystem DesignDevOpsEngineering GrowthBlog
Start learning
Chaturmind

Structured learning paths for engineers who want to go deep. Written by practitioners.

Learn

  • Java
  • DSA
  • System Design
  • Spring Boot
  • AI / ML
  • DevOps
  • Engineering Growth

Company

  • Blog
  • Contact

Legal

  • Privacy Policy
  • Terms of Service

© 2026 Chaturmind. All rights reserved.

Built for engineers who want to go deep.


← Java Concurrency & Multithreading

Threads & Runnable

  • Introduction to Threads
  • ExecutorService & Thread Pools
  • Fork/Join Framework

Synchronization & Memory Model

  • synchronized and Locks
  • volatile and the Memory Model
  • CompletableFuture
  • Deadlock, Starvation, Livelock

Concurrent Collections

  • ConcurrentHashMap & CopyOnWriteArrayList
  • HashMap Concurrency Variants
  • Concurrent Utilities & Coordination
Chaturmind
← Java Concurrency & Multithreading

Threads & Runnable

  • Introduction to Threads
  • ExecutorService & Thread Pools
  • Fork/Join Framework

Synchronization & Memory Model

  • synchronized and Locks
  • volatile and the Memory Model
  • CompletableFuture
  • Deadlock, Starvation, Livelock

Concurrent Collections

  • ConcurrentHashMap & CopyOnWriteArrayList
  • HashMap Concurrency Variants
  • Concurrent Utilities & Coordination
HomeLearnJavaJava Concurrency & MultithreadingConcurrent Collections
✓ FreeIntermediate· 11 min read

Concurrent Utilities & Coordination

CountDownLatch, CyclicBarrier, Semaphore, Exchanger, and the BlockingQueue family — the coordination primitives that sit above raw locks, plus a producer-consumer implementation.

Published September 22, 2026


Concurrent Utilities & Coordination

Locks and synchronized protect shared state. This lesson covers a different job: coordinating threads — making one thread wait for others, limiting concurrent access to a resource, or safely handing data between producer and consumer threads.

CountDownLatch — a one-time gate

CountDownLatch startGate = new CountDownLatch(3); // wait for 3 services

// Each service, on a separate thread, signals readiness
new Thread(() -> { initDatabase(); startGate.countDown(); }).start();
new Thread(() -> { initCache();    startGate.countDown(); }).start();
new Thread(() -> { initQueue();    startGate.countDown(); }).start();

startGate.await(); // blocks until count reaches 0
System.out.println("All services ready — starting server");

countDown() decrements the internal counter; await() blocks until it hits zero. The gate is one-time-use — once the count reaches zero, it stays open forever; there's no way to reset a CountDownLatch for a second round.

CyclicBarrier — a reusable rendezvous point

CyclicBarrier barrier = new CyclicBarrier(4, () -> System.out.println("Phase complete")); // 4 threads, + action on release

Runnable worker = () -> {
    doPhaseOneWork();
    try { barrier.await(); } catch (Exception e) { /* ... */ } // waits for all 4 to arrive
    doPhaseTwoWork();
    try { barrier.await(); } catch (Exception e) { /* ... */ } // reusable — same barrier, next phase
};

Unlike CountDownLatch, CyclicBarrier resets automatically once all parties arrive, ready to be reused for the next phase — the name literally means "cyclic." The typical fit: parallel computation with distinct phases, where every worker must finish phase N before any of them starts phase N+1 (e.g. a parallel matrix computation with dependent stages).

Semaphore — bounded concurrent access

Semaphore connectionLimiter = new Semaphore(10); // at most 10 concurrent DB connections

void queryDatabase() throws InterruptedException {
    connectionLimiter.acquire(); // blocks if 10 permits are already taken
    try {
        runQuery();
    } finally {
        connectionLimiter.release(); // ALWAYS release in finally
    }
}

A Semaphore isn't a lock in the mutual-exclusion sense — it allows up to N threads through simultaneously (a Semaphore(1) behaves like a lock, but the general case controls access to a pool of N interchangeable resources, e.g. a fixed number of DB connections, API rate slots, or worker permits). new Semaphore(n, true) requests fair ordering (FIFO — first thread to call acquire() gets the next available permit); the default is unfair, which is faster under low contention but can theoretically starve a long-waiting thread under sustained high contention.

Exchanger — pairwise data swap

Exchanger<List<String>> exchanger = new Exchanger<>();

// Thread A: fills a buffer, then swaps it for a fresh empty one from Thread B
List<String> bufferA = new ArrayList<>();
bufferA = exchanger.exchange(bufferA); // blocks until Thread B also calls exchange()

// Thread B: does the reverse
List<String> bufferB = new ArrayList<>();
bufferB = exchanger.exchange(bufferB);

Exchanger synchronizes exactly two threads at a rendezvous point where they swap objects — each thread's exchange() call blocks until the other thread also calls exchange(), then both return with the other's object. A niche tool, useful specifically for two-thread producer/consumer buffer-swapping designs (one thread fills a buffer while the other drains the previous one, then they trade).

The BlockingQueue family

BlockingQueue implementations add blocking put()/take() on top of a normal queue — put() blocks if the queue is full (for bounded queues), take() blocks if it's empty, which is exactly the coordination a producer-consumer setup needs without any manual wait()/notify().

  • ArrayBlockingQueue — fixed capacity, array-backed, FIFO.
  • LinkedBlockingQueue — optionally bounded (unbounded by default — watch for the same OOM risk as an unbounded ThreadPoolExecutor queue), linked-node-backed.
  • SynchronousQueue — capacity zero: every put() must be matched by a waiting take() before it returns — effectively a direct handoff between exactly two threads, not a buffer at all.
  • PriorityBlockingQueue — unbounded, orders elements by a Comparator instead of FIFO.

Producer-consumer with BlockingQueue

BlockingQueue<Task> queue = new ArrayBlockingQueue<>(100);

// Producer
new Thread(() -> {
    while (true) {
        Task task = generateTask();
        try { queue.put(task); } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; }
    }
}).start();

// Consumer
new Thread(() -> {
    while (true) {
        try {
            Task task = queue.take(); // blocks if empty
            process(task);
        } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; }
    }
}).start();

This is the entire pattern — no manual locking, no wait()/notify() (compare to the hand-rolled BoundedQueue with Conditions shown in synchronized and Locks). BlockingQueue implementations handle the full/empty coordination internally, which is exactly why they're the default choice over hand-rolling it.

Follow-up questions this topic invites — and their answers

Q: When would you choose Semaphore over a fixed-size thread pool for limiting concurrency? A: When you need to limit concurrent access to a resource that isn't naturally "a task running on a thread" — e.g. limiting concurrent outbound HTTP calls made from within otherwise-unrelated request-handling threads, where the threads themselves are already managed by a web server's own pool.

Q: Why does SynchronousQueue have zero capacity — what's it actually useful for? A: It's the queue implementation behind Executors.newCachedThreadPool() — a direct producer-to-consumer handoff with no buffering means a new thread is spun up immediately for a task if no idle thread is available to take() it right away, rather than the task sitting queued.

Q: What happens if you call CyclicBarrier.await() but one thread never arrives? A: Every other thread blocks indefinitely at the barrier (or until a supplied timeout on await(long, TimeUnit) elapses, throwing TimeoutException) — a CyclicBarrier provides no progress guarantee if a party fails to show up, which is why production use nearly always pairs it with a timeout and a BrokenBarrierException/TimeoutException handling path.

Q: Could you implement CountDownLatch's behavior with a Semaphore instead? A: Not cleanly — Semaphore doesn't have a "wait until N releases have happened" primitive matching await()'s all-or-nothing gate semantics; CountDownLatch is purpose-built for exactly that one-time "wait for N completions" shape, while Semaphore is purpose-built for "limit concurrent access to N." Different coordination shapes, not interchangeable despite both tracking a count internally.

Previous

HashMap Concurrency Variants

AI Tutor

Lesson: Concurrent Utilities & Coordination

Quick actions

AI responses can be inaccurate. Verify critical information.