Java PriorityBlockingQueue: Thread-Safe Priority Queue Examples

Java PriorityBlockingQueue examples with Comparator, take(), drainTo() and a print-server worker, plus how it differs from PriorityQueue and DelayQueue.

Two producer threads call put() on a PriorityBlockingQueue holding jobs with priorities 9, 5 and 1; a worker thread calls take() and gets the priority 9 job first. put() never blocks, and take() blocks only when the queue is empty.

The Java PriorityBlockingQueue class is a thread-safe, unbounded priority queue from java.util.concurrent whose take() method waits until an element is available. It orders elements the same way as PriorityQueue, by natural order or by a Comparator, so the head is always the least element.

We use a PriorityBlockingQueue when several threads add work and one or more worker threads should always pick the most important item first, such as a print server, a render farm or a background job runner with urgent and normal jobs.

The following example puts three numbers into the queue and reads them back in priority order, with the result of each line as a comment.

PriorityBlockingQueue<Integer> queue = new PriorityBlockingQueue<>();
queue.put(42);                                  // never blocks, the queue is unbounded
queue.put(8);
queue.put(17);
Integer head = queue.peek();                    // 8
Integer first = queue.take();                   // 8 (waits only when the queue is empty)
Integer second = queue.poll(1, TimeUnit.SECONDS);   // 17
int spaceLeft = queue.remainingCapacity();      // 2147483647 (Integer.MAX_VALUE)

Notice that put() returns at once, while take() and the timed poll() are the only methods that wait. We start with the features, look at natural and custom ordering, drainTo() and a blocking consumer, and finish with a print-server example, a comparison with other queues and common questions.

1. PriorityBlockingQueue Features

The class belongs to the concurrent part of the Java Collections Framework and is the thread-safe version of PriorityQueue. All public methods share one internal lock, so threads can call them at the same time without corrupting the heap. A few more rules of the PriorityBlockingQueue class matter in practice.

Two producer threads call put() on a PriorityBlockingQueue holding jobs with priorities 9, 5 and 1; a worker thread calls take() and gets the priority 9 job first. put() never blocks, and take() blocks only when the queue is empty.
Producers never wait on an unbounded PriorityBlockingQueue, and the worker always takes the highest-priority job first
  • The queue is unbounded. The initial capacity, 11 by default, only sizes the first array, and put() never blocks.
  • A null element makes put() throw a NullPointerException.
  • Without a Comparator, elements must implement Comparable, or put() throws a ClassCastException.
  • Equal elements come out in no guaranteed order, while the least element is always at the head.
  • Neither the iterator nor toString() follows priority order, whereas drainTo() removes elements in priority order.
  • The class is thread-safe, and the blocking methods respond to interrupts with an InterruptedException.

2. Java PriorityBlockingQueue Example

The examples use a print server that queues documents by priority. A record keeps each job small and immutable, which matters in a concurrent queue, because changing a field after the job is queued would break the heap order.

record PrintJob(String name, int priority) {}

2.1. Natural Ordering

Numbers and strings already implement Comparable, so a queue of them needs no extra code. The smallest value is the head.

PriorityBlockingQueue<String> names = new PriorityBlockingQueue<>(List.of("pear", "apple", "fig"));
String firstName = names.take();                // "apple"
String secondName = names.take();               // "fig"

2.2. PriorityBlockingQueue Comparator Example

For PrintJob, we want the highest priority number first, so we pass a comparator to the constructor PriorityBlockingQueue(int initialCapacity, Comparator comparator). Unlike PriorityQueue, the class has no constructor that takes only the comparator, so we always pass the initial capacity too.

Comparator<PrintJob> urgentFirst = Comparator.comparingInt(PrintJob::priority).reversed();
PriorityBlockingQueue<PrintJob> jobs = new PriorityBlockingQueue<>(11, urgentFirst);
jobs.put(new PrintJob("flyer", 1));
jobs.put(new PrintJob("invoice", 9));
jobs.put(new PrintJob("report", 5));
String next = jobs.take().name();               // "invoice"
String after = jobs.take().name();              // "report"

2.3. PriorityBlockingQueue drainTo() Example

The method drainTo() moves elements into another collection in one call and returns how many it moved. It removes them in priority order, which makes it the right way to get a sorted snapshot of the queue.

PriorityBlockingQueue<Integer> pending = new PriorityBlockingQueue<>(List.of(1, 3, 2, 6, 4, 5));
List<Integer> batch = new ArrayList<>();
int moved = pending.drainTo(batch, 3);          // 3
List<Integer> firstBatch = List.copyOf(batch);  // [1, 2, 3]
int movedRest = pending.drainTo(batch);         // 3
List<Integer> all = batch;                      // [1, 2, 3, 4, 5, 6]

The same method also moves jobs in batches out of a bounded ArrayBlockingQueue, where a worker processes up to N elements per call.

2.4. PriorityBlockingQueue Blocking Retrieval Example

A worker thread calls take() and waits while the queue is empty. When another thread puts a job, the worker wakes up and takes it. In the example, we start the worker first, put one job from the main thread and wait for the worker with join(), so the result does not depend on timing.

Comparator<PrintJob> urgentFirst = Comparator.comparingInt(PrintJob::priority).reversed();
PriorityBlockingQueue<PrintJob> inbox = new PriorityBlockingQueue<>(11, urgentFirst);
List<String> printed = new CopyOnWriteArrayList<>();

Thread printer = Thread.ofVirtual().start(() -> {
    try {
        PrintJob job = inbox.take();            // waits here until a job arrives
        printed.add(job.name());
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();     // keep the interrupt flag
    }
});
inbox.put(new PrintJob("invoice", 9));
printer.join();
List<String> done = List.copyOf(printed);       // [invoice]

The catch block restores the interrupt flag instead of printing a stack trace. That way, the code that interrupted the thread, for example an executor shutting down, can see that the worker stopped.

3. Java PriorityBlockingQueue Constructors

The class has four constructors. The capacity argument is only a starting size, because the queue grows as needed.

  • PriorityBlockingQueue() creates an empty queue with the initial capacity 11 and natural ordering.
  • PriorityBlockingQueue(int initialCapacity) creates an empty queue with natural ordering.
  • PriorityBlockingQueue(int initialCapacity, Comparator comparator) creates an empty queue ordered by the comparator.
  • PriorityBlockingQueue(Collection c) creates a queue that contains the elements of the collection. If the collection is a SortedSet or a PriorityQueue, the new queue keeps its order.

4. Java PriorityBlockingQueue Methods

The methods come from the BlockingQueue interface. Because the queue is unbounded, the insert methods behave the same, and only the retrieval methods differ in how they wait.

MethodWhen the queue is emptyNotes
put(e), offer(e), add(e)Not relevant, inserts at onceoffer() and add() always return true
take()Waits until an element arrivesThrows InterruptedException when interrupted
poll(timeout, unit)Waits up to the timeout and returns null if nothing arrivesGood for workers that also check a stop flag
poll()Returns null at onceDoes not wait
peek()Returns null at onceReads the head without removing it
drainTo(c), drainTo(c, max)Returns 0Moves elements in priority order
remainingCapacity()Always Integer.MAX_VALUEThe queue has no capacity limit
comparator()Returns the comparatorReturns null for natural ordering

The other methods, such as size(), contains(), remove(Object), clear() and toArray(), work as in any collection, and contains() and remove(Object) scan the whole array.

5. Print Server Job Queue

A print server loads the waiting jobs from the database at startup and hands them to one printer thread. The printer must finish urgent documents first and stop when the queue is done. We add a stop marker with the lowest possible priority, so it comes out after every real job. This marker is often called a poison pill.

Comparator<PrintJob> urgentFirst = Comparator.comparingInt(PrintJob::priority).reversed();
PrintJob stop = new PrintJob("STOP", Integer.MIN_VALUE);
PriorityBlockingQueue<PrintJob> spool = new PriorityBlockingQueue<>(11, urgentFirst);
spool.put(new PrintJob("flyer", 1));
spool.put(new PrintJob("contract", 9));
spool.put(new PrintJob("report", 5));
spool.put(new PrintJob("payslip", 7));
spool.put(stop);

List<String> printOrder = new CopyOnWriteArrayList<>();
Thread worker = Thread.ofVirtual().start(() -> {
    try {
        PrintJob job;
        while ((job = spool.take()) != stop) {
            printOrder.add(job.name());
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
});
worker.join();
List<String> printedJobs = List.copyOf(printOrder);   // [contract, payslip, report, flyer]

A PriorityBlockingQueue never pushes back on producers, so a fast producer and a slow consumer make the queue grow until the app runs out of memory. When producers can outrun the workers, we limit the number of queued jobs, for example with a Semaphore that a producer acquires before put() and the worker releases after take().

Two jobs with the same priority come out in no fixed order. When arrival order matters, we add a sequence number as the second sort key, the same fix we use for ties in a PriorityQueue.

record SequencedJob(PrintJob job, long sequence) {}
AtomicLong sequence = new AtomicLong();
PriorityBlockingQueue<SequencedJob> fair = new PriorityBlockingQueue<>(11,
        Comparator.comparingInt((SequencedJob s) -> s.job().priority()).reversed()
                .thenComparingLong(SequencedJob::sequence));
fair.put(new SequencedJob(new PrintJob("memo-a", 3), sequence.getAndIncrement()));
fair.put(new SequencedJob(new PrintJob("memo-b", 3), sequence.getAndIncrement()));
String firstMemo = fair.take().job().name();    // "memo-a"

6. PriorityBlockingQueue vs PriorityQueue and Other Blocking Queues

The right queue depends on two questions, namely whether several threads share it and whether the order is priority, time or arrival.

ClassThread-safeOrderBoundedWaiting methods
PriorityQueueNoPriorityNoNone
PriorityBlockingQueueYesPriorityNotake(), timed poll()
DelayQueueYesEarliest expired delayNotake() waits for the delay
ArrayBlockingQueueYesFIFOYesput() and take()
LinkedBlockingQueueYesFIFOOptionalput() and take()

We pick PriorityBlockingQueue when importance decides the order, DelayQueue when a time decides it, and a bounded FIFO queue when producers must slow down once the workers fall behind.

7. PriorityBlockingQueue FAQs

Most questions about PriorityBlockingQueue concern its capacity, the waiting methods and the use in a thread pool.

7.1. Is PriorityBlockingQueue Bounded?

No. The capacity passed to the constructor is only the initial array size. The method remainingCapacity() always returns Integer.MAX_VALUE, and put() never blocks.

7.2. What Is the Difference Between take() and poll()?

The method take() waits until an element is available, whereas poll() returns null at once on an empty queue. The timed poll(timeout, unit) returns null when the timeout passes without an element.

PriorityBlockingQueue<Integer> idle = new PriorityBlockingQueue<>();
Integer none = idle.poll();                         // null
Integer timedOut = idle.poll(10, TimeUnit.MILLISECONDS);   // null after 10 ms

7.3. Why Does Printing a PriorityBlockingQueue Not Show Sorted Order?

Internally, the elements form a binary heap, and toString() prints the heap array. Only the head is guaranteed to be the least element. To see the elements in order, we call drainTo() or sort a copy.

PriorityBlockingQueue<Integer> heap = new PriorityBlockingQueue<>(List.of(42, 17, 99, 8, 23));
List<Integer> sortedCopy = heap.stream().sorted().toList();   // [8, 17, 23, 42, 99]

7.4. Can Several Consumers Take From the Same Queue?

Yes. Each element goes to one consumer only, and every take() call returns the current head. With several consumers, the jobs start in priority order, but they can finish in any order because the threads run in parallel.

7.5. Should I Use PriorityBlockingQueue With a ThreadPoolExecutor?

We can, because ThreadPoolExecutor accepts any BlockingQueue<Runnable>. The tasks must be comparable, and tasks submitted with submit() are wrapped in a FutureTask that is not Comparable, so we pass our own comparable tasks to execute() instead.

8. Conclusion

The PriorityBlockingQueue class combines the heap order of PriorityQueue with thread safety and a waiting take(). Producers call put() and never wait, workers call take() or a timed poll(), and the head is always the least element by natural order or by the Comparator.

Because the queue is unbounded, we watch its size or limit producers ourselves. For ordered snapshots we use drainTo(), for equal priorities a sequence number, and for a clean shutdown a poison pill or an interrupt.

9. References

Happy Learning !!

Source Code on Github

Leave a Comment

Comments are closed.

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.