skip to content

Concurrency Utilities (Locks & Synchronizers)

The explicit locks and synchronizer classes in java.util.concurrent that go beyond what synchronized can express: timeouts, fairness, multiple conditions, permits and barriers. Interviewers ask when you would step up from synchronized to these.

part ofJavaoverview, primer and where to startread it →
on this pageshow

explore

questions

24

What is a CountDownLatch and how do you use it?

level: juniorimportance: must knowfreq 70%

answer

  1. One-shot counting gate
  2. await() blocks, countDown() decrements
  3. Released at zero, never resets
  4. Start gate (count=1) vs wait-for-completion (count=N)
  5. Any thread can countDown()

basics

~20 s

A CountDownLatch is a one-time gate. You start it with a count. Threads call await() to wait; other code calls countDown() to lower the count. When it hits zero, every waiter is released and can continue.

solid answer

~40 s

CountDownLatch is a one-shot synchronizer from java.util.concurrent. You construct it with an integer count. One or more threads call await(), which blocks until the count reaches zero. Other threads call countDown(), each call decrementing the count by one. When it hits zero, all waiting threads are released at once and any later await() returns immediately. The classic use is making a main thread wait until N worker threads have each signalled completion, or holding N workers at a starting gate until the main thread counts down a single latch to start them together. It is single-use: once the count reaches zero it stays there and cannot be reset. countDown() can be called by any thread, and the threads that count down need not be the threads that await.

code

java · 13 lines
java
CountDownLatch latch = new CountDownLatch(3);
for (int i = 0; i < 3; i++) {
    int id = i;
    new Thread(() -> {
        try {
            doWork(id);
        } finally {
            latch.countDown();   // signal this worker is done
        }
    }).start();
}
latch.await();                   // main thread blocks until count == 0
System.out.println("all workers finished");

go deeper

for a junior

Knows it is a counter you await() on and countDown(), released at zero, used to wait for workers.

for a middle

Distinguishes the start-gate (count=1) vs wait-for-completion (count=N) patterns and uses a timeout-bounded await; puts countDown() in finally.

for a senior

Explains the happens-before visibility guarantee and the one-shot semantics, and chooses latch vs barrier deliberately.

for a principal

Designs coordination primitives, weighs latch against CompletableFuture/phaser-based approaches, and reasons about failure modes (dead worker hangs the waiter) and observability.

## The problem it solves In concurrent programs you often need one thread to **wait until a set of events has happened** before continuing. For example, a main thread may launch several worker threads and must not proceed until all workers have finished initialising. Without a tool you'd resort to fragile loops checking a shared flag. `CountDownLatch` is a purpose-built tool for this. ## What it is `java.util.concurrent.CountDownLatch` is a **synchronizer** — a small helper that coordinates the timing of threads. Think of it as a **counter with a gate**: - You create it with a starting number: `new CountDownLatch(3)`. - Threads that want to wait call **`await()`**. This **blocks** the calling thread (it parks, using no CPU) until the counter reaches zero. - Any code calls **`countDown()`**, which **decreases the counter by one**. - The instant the counter reaches **zero**, every thread blocked in `await()` is **released simultaneously**, and any future `await()` returns immediately. ### Key terms defined - **Block / await**: the thread is suspended by the OS scheduler and consumes no CPU while waiting; it resumes when signalled. - **Synchronizer**: a class whose only job is to control when threads may proceed relative to each other. - **One-shot**: the counter only goes down, never resets. Once at zero it stays at zero forever. ## The two classic patterns 1. **Wait for completion** — count = N (number of workers). The coordinator calls `await()`; each worker calls `countDown()` when it finishes. The coordinator wakes only after all N have finished. 2. **Start gate** — count = 1. All workers call `await()` first, blocking at a starting line; the coordinator does setup then calls `countDown()` once, releasing every worker at the same moment for a synchronized start. ## Important details - `countDown()` can be called by **any** thread, and **never blocks** — it just decrements. Extra calls below zero are harmless no-ops. - The threads doing `countDown()` do **not** have to be the threads doing `await()`. - `getCount()` returns the current count (useful for debugging, not for control logic). - `await(timeout, unit)` waits at most a bounded time and returns `true` if the latch opened, `false` on timeout — important so you never wait forever if a worker dies. - **Memory visibility**: actions in a thread before its `countDown()` *happen-before* actions after a successful `await()` in another thread. So results published by workers are safely visible to the waiter. ## When NOT to use it If you need to reuse the barrier across multiple rounds, `CountDownLatch` cannot reset — use `CyclicBarrier` (reusable) instead.

  • What happens if you call await() after the count has already reached zero?
    It returns immediately without blocking, because the latch is already open and stays open permanently.
  • Can the same threads that await() also call countDown()?
    Yes, there is no rule tying the two. countDown() is independent of await(); any thread can call either.

A relay race official holding runners at the line: when the official's count reaches zero (the starting gun), everyone is released at once. The gun can only be fired once — you can't 'un-fire' it for the next race.

saying these in an interview costs you the question

  • Claiming a CountDownLatch can be reset or reused after reaching zero
  • Thinking await() returns when count drops by one rather than when it reaches zero
  • Believing only the waiting threads may call countDown()
  • Forgetting countDown() in a finally block, so a thrown exception leaves the waiter hung forever

context

open as a page

What is a Semaphore in Java and what problem does it solve?

level: juniorimportance: must knowfreq 70%

basics

~20 s

A Semaphore holds a set number of permits. A thread calls acquire() to take a permit (waiting if none are free) and release() to give it back. It limits how many threads can use a resource at once.

open as a page

What is a CyclicBarrier and what is its barrier action?

level: middleimportance: must knowfreq 62%

basics

~20 s

A CyclicBarrier is a reusable meeting point for a fixed number of threads. Each thread calls await() and blocks until all of them arrive. Then they all proceed, and the barrier resets so it can be used again for the next round.

open as a page

What is a ReadWriteLock and when would you use one instead of a plain lock or synchronized?

level: middleimportance: must knowfreq 62%

basics

~20 s

A ReadWriteLock has two locks: a read lock many threads can hold at once, and a write lock only one thread can hold (with no readers at the same time). Use it when data is read far more often than written, so reads don't block each other.

open as a page

What does tryLock() do, and how do its non-blocking and timed forms help avoid deadlock?

level: middleimportance: must knowfreq 70%

basics

~20 s

tryLock() tries to grab the lock and returns true or false right away instead of waiting. tryLock(time, unit) waits only up to a time limit. This lets a thread give up instead of blocking forever, so threads can back off and avoid getting stuck in a deadlock.

open as a page

What is ReentrantLock, and how does it differ from the synchronized keyword?

level: middleimportance: must knowfreq 78%

basics

~20 s

ReentrantLock is a lock object you control with explicit lock() and unlock() calls. synchronized is a keyword that locks and unlocks automatically. ReentrantLock adds extra abilities like timed and interruptible locking, but you must remember to unlock it yourself in a finally block.

open as a page

Show how to use a Semaphore to bound concurrent access to a resource pool, and explain the correctness pitfalls.

level: middleimportance: must knowfreq 60%

basics

~20 s

Create a Semaphore with as many permits as the resource allows. Each user acquires a permit before using the resource and releases it in a finally block afterward. That caps the number of simultaneous users at the permit count.

open as a page

What are the key differences between CountDownLatch and CyclicBarrier, and when would you choose each?

level: seniorimportance: must knowfreq 78%

basics

~20 s

A CountDownLatch is one-shot: any thread counts it down and waiters proceed at zero. A CyclicBarrier is reusable: a fixed set of threads wait for each other, then all go together and it resets. Use a latch for a single start/done signal, a barrier for repeated lockstep rounds.

open as a page

What is a Condition obtained from a Lock, and why can it be better than wait()/notify() on a monitor?

level: seniorimportance: must knowfreq 62%

basics

~20 s

A Condition is a waiting area tied to a Lock, created with lock.newCondition(). Threads call await() to wait and signal()/signalAll() to wake them — like wait()/notify() but you can have several separate waiting areas per lock, so you can wake exactly the right group of waiters.

open as a page

How does tryAcquire() differ from acquire(), and when would you use the timed or multi-permit variants?

level: middleimportance: should knowfreq 55%

basics

~20 s

acquire() waits until a permit is free. tryAcquire() takes a permit only if one is available right now and returns true; otherwise it returns false immediately without waiting. tryAcquire(timeout) waits up to a limit before giving up.

open as a page

How should you handle interruption, timeouts, and barrier breakage when waiting on these synchronizers?

level: seniorimportance: should knowfreq 45%

basics

~20 s

await() can throw InterruptedException, so handle it and usually re-set the interrupt flag. Prefer the timed await so a dead worker can't hang you forever. For CyclicBarrier also catch BrokenBarrierException, which means another party failed and the barrier is now broken.

open as a page

Given a multi-stage parallel job where workers may join or finish between stages, which coordination primitive would you choose and why?

level: seniorimportance: should knowfreq 26%

basics

~10 s

Use a Phaser. It is reusable for the multiple stages and lets workers register or deregister between stages, which CountDownLatch (one-shot) and CyclicBarrier (fixed count) cannot do.

open as a page

What is a Phaser and how does it improve on CyclicBarrier and CountDownLatch?

level: seniorimportance: should knowfreq 30%

basics

~20 s

A Phaser is a reusable barrier where threads wait for each other at the end of each phase. Unlike CountDownLatch and CyclicBarrier, the number of parties can grow or shrink at runtime, and it supports many phases.

open as a page

How do you choose between synchronized, ReentrantReadWriteLock, and StampedLock for protecting shared state?

level: seniorimportance: should knowfreq 44%

basics

~20 s

Use synchronized by default — it's simplest. Move to ReentrantReadWriteLock when reads greatly outnumber writes and you want readers to run in parallel. Use StampedLock for very hot, short read paths where you can use optimistic reads, but only if you don't need reentrancy or condition waiting.

open as a page

Why is it dangerous that StampedLock is non-reentrant, and what bug does this commonly cause?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Non-reentrant means a thread that already holds the lock cannot acquire it again — if it tries, it blocks waiting for itself and deadlocks. So a method holding the write lock that calls another method which also takes the lock will freeze.

open as a page

How does StampedLock's optimistic read mode work, and how is it different from acquiring a read lock?

level: seniorimportance: should knowfreq 48%

basics

~20 s

An optimistic read takes no lock at all. You get a 'stamp' (a number), read the data, then call validate(stamp). If no writer changed the data in between, validate returns true and your read was fine; if it returns false, you fall back to a real read lock and read again.

open as a page

What is the fairness option on ReentrantLock, and what is the trade-off of enabling it?

level: seniorimportance: should knowfreq 48%

basics

~20 s

A fair ReentrantLock (new ReentrantLock(true)) hands the lock to waiting threads in the order they asked for it, so no thread is starved. The default unfair lock lets whoever grabs it first win, which is faster overall but can leave some threads waiting longer.

open as a page

What is the difference between lock(), lockInterruptibly(), and tryLock() with respect to interruption and blocking?

level: seniorimportance: should knowfreq 44%

basics

~20 s

lock() waits for the lock no matter what and ignores interruption while waiting. lockInterruptibly() waits but throws InterruptedException if the thread is interrupted, so the wait can be cancelled. tryLock() doesn't wait at all (or only up to a timeout) and returns true/false.

open as a page

What does the fairness flag on a Semaphore control, and what is the tradeoff?

level: seniorimportance: should knowfreq 45%

basics

~20 s

The fairness flag decides the order waiting threads get permits. Fair (true) serves them first-come-first-served (FIFO), preventing any thread from waiting forever. Non-fair (the default) lets a new thread grab a free permit ahead of those already waiting, which is faster but can starve waiters.

open as a page

When would you choose a Semaphore over a lock, CountDownLatch, or BlockingQueue — and what are its limitations?

level: seniorimportance: should knowfreq 50%

basics

~20 s

Use a Semaphore when you need to limit how many threads use something at once (N at a time). Use a lock for exclusive access by one thread. Use a CountDownLatch to wait until events finish. Use a BlockingQueue to hand data between threads. A Semaphore only counts permits; it doesn't track owners or carry data.

open as a page

What is java.util.concurrent.Exchanger and when would you use it?

level: middleimportance: nice to knowfreq 18%

basics

~10 s

Exchanger is a meeting point where exactly two threads swap objects. Each thread calls exchange(myValue); both block until the partner arrives, then each receives what the other passed in.

open as a page

When do CountDownLatch and CyclicBarrier fall short, and what would you reach for instead?

level: principalimportance: nice to knowfreq 30%

basics

~20 s

They fall short when the number of participants changes over time or when you want to compose async results without blocking threads. For dynamic, multi-phase coordination use Phaser; for non-blocking result composition use CompletableFuture or structured concurrency.

open as a page

Explain how a Phaser's phase number works and how termination changes the behavior of its await methods.

level: principalimportance: nice to knowfreq 14%

basics

~10 s

Each completed round increments the phase number (an int starting at 0). When the phaser terminates, await methods stop blocking and immediately return a negative number, so finishing threads can't deadlock the rest.

open as a page

What modes does StampedLock support and how does mode conversion (e.g. tryConvertToWriteLock) work?

level: principalimportance: nice to knowfreq 24%

basics

~20 s

StampedLock has three modes: writing (exclusive), reading (shared), and optimistic reading (no lock). Conversion methods like tryConvertToWriteLock take your current stamp and try to switch modes; they return a new stamp on success or 0 on failure, so you must check the result.

open as a page