skip to content

How do DelayQueue and PriorityBlockingQueue differ from a plain FIFO BlockingQueue?

level: seniorimportance: should knowfreq 48%

answer

  1. Neither is FIFO; both are unbounded
  2. Priority = heap, take() returns least by Comparable/Comparator
  3. DelayQueue elements implement Delayed (getDelay)
  4. Unexpired DelayQueue looks empty to poll/take
  5. Uses: priority scheduling; TTL/retry/backoff timing

basics

~20 s

Neither is FIFO. PriorityBlockingQueue returns elements in priority order (a heap), and is unbounded. DelayQueue holds Delayed elements that only become available for take() after their delay expires; until then the queue acts empty even if it has elements.

solid answer

~40 s

Both are ordered, unbounded BlockingQueues that drop FIFO. PriorityBlockingQueue is a thread-safe priority heap: take() always returns the least element by natural ordering or a Comparator, not insertion order. It's unbounded (it grows), so put() never blocks — only take() blocks when truly empty. DelayQueue holds elements implementing the Delayed interface; each reports its remaining delay via getDelay(). An element can only be taken once its delay has elapsed: take() returns the element whose delay expired earliest, and blocks (even with elements present) until the head's delay reaches zero — so an unexpired queue 'looks empty' to take()/poll(). Internally DelayQueue orders by delay using a priority heap. Typical uses: PriorityBlockingQueue for priority scheduling of tasks; DelayQueue for scheduled/expiring work like cache eviction, retry-after-backoff, or session timeouts (it underpins ScheduledThreadPoolExecutor's delayed queue concept).

code

java · 18 lines
java
// PriorityBlockingQueue: highest priority (lowest comparator value) comes out first
record Job(int priority, String name) {}
BlockingQueue<Job> pq =
    new PriorityBlockingQueue<>(11, Comparator.comparingInt(Job::priority));
pq.put(new Job(5, "low"));
pq.put(new Job(1, "urgent"));
System.out.println(pq.take().name()); // "urgent" — not insertion order

// DelayQueue: element released only after its delay elapses
class Expiring implements Delayed {
    final long readyAt; final String id;
    Expiring(String id, long delayMs) { this.id = id; this.readyAt = System.nanoTime() + delayMs * 1_000_000; }
    public long getDelay(TimeUnit u) { return u.convert(readyAt - System.nanoTime(), TimeUnit.NANOSECONDS); }
    public int compareTo(Delayed o) { return Long.compare(getDelay(TimeUnit.NANOSECONDS), o.getDelay(TimeUnit.NANOSECONDS)); }
}
DelayQueue<Expiring> dq = new DelayQueue<>();
dq.put(new Expiring("a", 200));
// dq.take() blocks ~200ms even though 'a' is already in the queue

go deeper

for a junior

Knows PriorityBlockingQueue returns items by priority and DelayQueue releases items only after a delay; both differ from FIFO.

for a middle

Explains the heap ordering, the Delayed interface and getDelay, that an unexpired DelayQueue acts empty, and that both are unbounded so put doesn't block.

for a senior

Maps each to real use cases (priority scheduling, TTL/eviction, retry-backoff), knows the iterator/stability caveats, and the lack of backpressure from unboundedness.

for a principal

Designs scheduling/expiry subsystems around these (or chooses a timing wheel/ScheduledExecutor instead), and addresses the unbounded-memory risk with explicit producer throttling.

## Plain BlockingQueues are FIFO; these two are not ArrayBlockingQueue and LinkedBlockingQueue return elements in **arrival order** (FIFO). The two queues here reorder elements by a *property*, and both are **unbounded** (so `put` never blocks — backpressure must come from elsewhere). ## PriorityBlockingQueue — ordered by priority This is the thread-safe, blocking version of `PriorityQueue`. It keeps elements in a **binary heap** so that `take()`/`poll()` always returns the **smallest** element according to either: - the elements' **natural ordering** (they implement `Comparable`), or - a **`Comparator`** you pass to the constructor. ('Smallest' = highest priority by convention; invert the comparator for largest-first.) Key facts: - **Unbounded**: it grows as needed, so `put`/`offer` **never block or fail** for capacity; `take` blocks only when the queue is genuinely empty. The capacity arg to the constructor is just an *initial* size hint. - **Ordering caveat**: only the head is guaranteed to be the minimum. The `iterator()` and `toArray()` do **not** traverse in sorted order, and elements that compare equal have no guaranteed relative order (not stable). - **Use it** to process work by importance — e.g. high-priority jobs jump ahead of low-priority ones. ## DelayQueue — ordered by 'available at' time DelayQueue holds elements that implement the **`Delayed`** interface (which extends `Comparable<Delayed>`). Each element answers one question via `getDelay(TimeUnit unit)`: *how much time remains before I'm allowed to be taken?* A return value ≤ 0 means 'ready now'. The defining rule: **an element can only be removed once its delay has expired.** - `take()` returns the element whose delay expired **earliest**; if the head element's delay hasn't elapsed yet, `take()` **blocks until it does** — even though the queue physically contains elements. - Therefore an unexpired DelayQueue **behaves as empty** to consumers: `poll()` returns `null`, and `peek()` may show the head but `poll`/`take` won't release it early. - Internally it orders elements by delay using a priority heap, so the soonest-ready element is always at the front. - It is **unbounded**. Think of it as a 'release at time T' mailbox: you drop letters in with a 'do not open before' date; a reader can only pull a letter out once that date has passed, soonest-due first. ## When to use which | Need | Use | |---|---| | Process items by **importance/priority** | **PriorityBlockingQueue** | | Make items available only **after a delay / at a scheduled time** | **DelayQueue** | | Cache entry **expiration / TTL eviction** | DelayQueue (entry's delay = time-to-live) | | **Retry with backoff** (don't retry before time X) | DelayQueue | | Plain order, no priority/timing | ArrayBlockingQueue / LinkedBlockingQueue | DelayQueue is conceptually what scheduling executors use to release tasks at their due time (ScheduledThreadPoolExecutor uses a specialized delayed work queue). ## Shared gotcha: unbounded means no built-in backpressure Because both are unbounded, a fast producer can grow them without limit and exhaust memory; `put` will never throttle it. If you need a memory ceiling you must bound producers yourself (e.g. a semaphore) — these queues won't do it for you.

  • If a DelayQueue has elements but none have expired, what does poll() return?
    null — the queue behaves as empty until the head element's delay reaches zero, even though it physically contains elements.
  • Does put() ever block on a PriorityBlockingQueue?
    No. It's unbounded and grows on demand, so put/offer never block for capacity; only take blocks, and only when the queue is genuinely empty.

saying these in an interview costs you the question

  • Thinking DelayQueue's poll() returns an element before its delay expires
  • Assuming PriorityBlockingQueue's iterator is in sorted order
  • Believing these queues can be bounded / apply backpressure
  • Forgetting elements must implement Comparable or Delayed (else ClassCastException / contract break)

context