Difference between PriorityQueue and PriorityBlockingQueue. — Cracked Java
// Java Collections Framework · Queue, Deque, ArrayDeque, PriorityQueue
MidTheory

Difference between PriorityQueue and PriorityBlockingQueue.

PriorityQueue is unbounded and not thread-safe. PriorityBlockingQueue is unbounded, thread-safe, and adds blocking semanticstake() blocks while empty, letting consumer threads sleep efficiently until producers add work. Both are heap-based with the same priority semantics; the difference is concurrency.

Side-by-side

AspectPriorityQueuePriorityBlockingQueue
Packagejava.utiljava.util.concurrent
Thread-safeNoYes (ReentrantLock internally)
Bounded?No (grows on add)No (grows on add)
Blocks on empty take?N/A — poll returns nulltake() blocks until non-empty
Blocks on full put?N/A — never fullN/A — never full (unbounded)
Allows null?No (NPE)No (NPE)
Iterator orderHeap order (undefined for priority)Weakly consistent snapshot, heap order
Initial capacity1111
Comparator supportYesYes

Why "unbounded but blocking"

BlockingQueue traditionally implies bounded — ArrayBlockingQueue(int capacity) blocks producers when full. PriorityBlockingQueue is unusual: it blocks consumers on empty (the useful case for priority work) but never blocks producers, growing the heap as needed.

The trade-off: producers can't apply back-pressure via the queue. If a producer outpaces consumers indefinitely, the heap grows until OOM. Pair it with explicit rate limiting or a Semaphore.

Code: producer-consumer with priorities

public record Task(int priority, String name) implements Comparable<Task> {
    @Override
    public int compareTo(Task other) {
        return Integer.compare(this.priority, other.priority); // lower = higher priority
    }
}

PriorityBlockingQueue<Task> queue = new PriorityBlockingQueue<>();

// Producer
Thread producer = new Thread(() -> {
    queue.put(new Task(5, "low"));     // never blocks (unbounded)
    queue.put(new Task(1, "urgent"));
    queue.put(new Task(3, "medium"));
});

// Consumer
Thread consumer = new Thread(() -> {
    try {
        while (true) {
            Task t = queue.take();  // blocks on empty
            System.out.println("Processing: " + t.name());
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
});

The consumer wakes up only when there's work, with the highest-priority task immediately available — no busy-wait, no poll() == null polling loop.

When to use which

  • Single-threaded priority work (sort by priority, top-k, Dijkstra, A*) — PriorityQueue. Faster, no locking overhead.
  • Multiple consumer threads pulling priority-ordered work — PriorityBlockingQueue. Built-in blocking + lock-based safety.
  • High-contention priority work where you want lock-free — neither. There's no JDK-provided lock-free priority queue. Consider ConcurrentSkipListMap keyed by priority for a sorted-but-concurrent alternative.

Internal locking strategy

PriorityBlockingQueue uses a single ReentrantLock guarding the heap. All put, offer, poll, take, peek, and iterator acquire this lock. This makes operations atomic but serializes them — high contention with many producers hurts throughput.

For very high contention, look at LinkedTransferQueue (FIFO only) or partition work across multiple queues by priority class.

Take with timeout

Task t = queue.poll(500, TimeUnit.MILLISECONDS);
if (t == null) {
    // timeout — no work after 500ms
} else {
    process(t);
}

This is the standard pattern for shutdown-aware consumers: poll with timeout, check shutdown flag, loop.

Mark your status