How do DelayQueue and PriorityBlockingQueue differ from a plain FIFO BlockingQueue?
answer
- Neither is FIFO; both are unbounded
- Priority = heap, take() returns least by Comparable/Comparator
- DelayQueue elements implement Delayed (getDelay)
- Unexpired DelayQueue looks empty to poll/take
- Uses: priority scheduling; TTL/retry/backoff timing
basics
~20 sNeither 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 sBoth 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// 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 queuego deeper
Knows PriorityBlockingQueue returns items by priority and DelayQueue releases items only after a delay; both differ from FIFO.
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.
Maps each to real use cases (priority scheduling, TTL/eviction, retry-backoff), knows the iterator/stability caveats, and the lack of backpressure from unboundedness.
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)