skip to content

What is a SynchronousQueue and when is it the right choice?

level: seniorimportance: should knowfreq 55%

answer

  1. Zero capacity — stores nothing
  2. put waits for take, take waits for put (rendezvous)
  3. size()==0, isEmpty()==true, peek()==null always
  4. Backs newCachedThreadPool (hand-off or new thread)
  5. Fair (FIFO) vs default (LIFO/stack) waiter policy

basics

~20 s

A SynchronousQueue has zero capacity: it holds no elements. Each put must wait for a matching take (and vice versa) — it's a direct hand-off between two threads. Use it when you want producers and consumers to rendezvous with no buffering.

solid answer

~50 s

SynchronousQueue is a BlockingQueue with no internal capacity — it stores nothing. Every insert is a direct hand-off: a put() blocks until some thread does a take(), and a take() blocks until some thread does a put(); the two rendezvous and the element passes straight across. Because there's no buffer, isEmpty() is always true, size() is 0, and peek() returns null. It supports an optional fairness mode (FIFO vs LIFO waiter ordering). Its classic use is as the work queue of a cached thread pool: Executors.newCachedThreadPool uses a SynchronousQueue so a submitted task is either handed to an immediately-available idle worker or triggers creation of a new thread — tasks are never queued. It's ideal when you want immediate hand-off with built-in backpressure and no buffering, or when buffering would hide latency. The downside: if no consumer is ready, every producer blocks (or, with offer(), fails immediately).

code

java · 13 lines
java
SynchronousQueue<String> sq = new SynchronousQueue<>();

// Consumer thread: blocks until a producer hands off
new Thread(() -> {
    try { System.out.println("got: " + sq.take()); }
    catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}).start();

// Producer: put() blocks until the consumer above is ready to take
sq.put("hello");          // direct hand-off, no buffering

System.out.println(sq.size());      // always 0
System.out.println(sq.offer("x"));  // false: no consumer waiting right now

go deeper

for a junior

Knows it's a zero-capacity queue used for direct thread-to-thread hand-off.

for a middle

Explains that put/take must rendezvous, that size is always 0, and that offer/poll only succeed when a counterpart is waiting.

for a senior

Articulates the cached-thread-pool use case, the backpressure/no-buffering trade-off, fairness modes, and the indefinite-block pitfall with mitigations.

for a principal

Decides hand-off vs buffered designs at the architecture level, reasons about latency vs burst-smoothing, and pairs queue choice with pool sizing and rejection policy for system stability.

## A queue that holds nothing Most queues *store* elements. **SynchronousQueue is different: its capacity is zero.** It never holds any element. Instead it is a **rendezvous point** (a 'meeting') between a producer and a consumer. - A **`put(e)`** does not deposit `e` into a buffer. It **blocks** until another thread calls `take()`; at that moment the element is handed **directly** from the putting thread to the taking thread, and both proceed. - Symmetrically, a **`take()`** blocks until some thread calls `put(e)`. Think of it as a baton pass in a relay race: the runner holding the baton (producer) must wait until the next runner (consumer) is right there to receive it; neither can put the baton 'down' on a table in between. ## Observable consequences of zero capacity Because nothing is ever stored: - `size()` always returns **0**. - `isEmpty()` always returns **true**. - `peek()` always returns **null** (there's never a head to look at). - `iterator()` yields nothing. - `offer(e)` (non-blocking) returns `true` **only if** a consumer is already waiting to take, otherwise `false` immediately. - `poll()` returns an element **only if** a producer is already waiting, otherwise `null`. ## Fairness mode The constructor takes an optional `fair` boolean. When `fair=true`, waiting threads are served in **FIFO** order (the longest-waiting producer/consumer matches first); when `false` (default), it uses a LIFO-ish stack policy, which can have higher throughput but may starve some waiters. ## The canonical use: cached thread pool `Executors.newCachedThreadPool()` backs its `ThreadPoolExecutor` with a `SynchronousQueue`. The effect: when a task is submitted, the executor tries to hand it to an **idle worker** waiting in `take()`. If one is available, instant hand-off. If none is available, the offer fails, and the executor **spins up a new thread** instead of queuing the task. This gives a pool that grows on demand and shrinks when idle — appropriate for many short-lived tasks — precisely *because* the queue refuses to buffer. ## When to choose it Pick SynchronousQueue when: - You want **direct hand-off** with **maximum backpressure**: a producer cannot get ahead of consumers at all. - **Buffering would be harmful** — e.g. it would hide that consumers are overloaded, or add latency to data that must be processed *now*. - You want a thread pool that prefers creating/reusing threads over building a backlog (cached pool semantics). Avoid it when producers and consumers run at uneven rates and you actually *want* some buffering to smooth bursts — there, a bounded ArrayBlockingQueue is better. ## Pitfall With SynchronousQueue, a `put()` with no waiting consumer **blocks indefinitely**. If that's not acceptable, use the timed `offer(e, timeout, unit)` or non-blocking `offer(e)` and handle the 'no taker' case. Likewise, pairing a SynchronousQueue with a fixed thread pool of size N means at most N tasks can ever be in flight and the (N+1)th submission is rejected or blocks per the rejection policy.

  • Why does newCachedThreadPool use a SynchronousQueue instead of a LinkedBlockingQueue?
    So tasks are never queued: each submission is either handed directly to an idle worker or causes a new thread to be created. The non-buffering hand-off is what makes the pool grow on demand and avoid a hidden backlog.
  • What does offer() return on a SynchronousQueue when no consumer is waiting?
    false, immediately — because there is no buffer to place the element in and no taker to receive it.

saying these in an interview costs you the question

  • Saying it holds one element — it holds zero
  • Thinking offer() always succeeds — it only succeeds if a taker is waiting
  • Using it where buffering of bursts is actually desired
  • Forgetting put() blocks forever with no consumer (use timed offer)

context