What is a Java queue?

A Java queue holds elements in the order they arrived and hands them back out from the front, oldest first. This page covers the java.util.Queue interface and its methods, the implementations the JDK ships with and how to choose between them, which of them are safe to share between threads, and how to share a single queue across several servers using Valkey or Redis.

What is a queue in Java?

A queue is an abstract data type that keeps track of objects in a sequence based on a FIFO (first in, first out) order. New elements are always added at the end of the queue, called the tail, and are always removed from the front, called the head. Queues model the behaviour of waiting in line at a bank or an office: whoever arrives first is served first.

The value of a queue in software is that it decouples the code producing work from the code performing it. A web application under load can accept incoming requests and place them on a queue rather than handling each one immediately, letting a fixed number of workers drain the queue at whatever rate they can sustain. The producer never waits for the consumer, and the consumer never has to keep up with a burst.

Java queues are implementations of that data type in the Java programming language. Every Java queue is built around the java.util.Queue interface, which defines the behaviour that all of them share.

Java queue methods and the Queue interface

The java.util.Queue interface extends Collection and adds six methods, arranged as three operations in two forms. One form throws an exception on failure, the other returns a special value:

OperationThrows an exceptionReturns a special value
Insertadd(e)offer(e)
Removeremove()poll()
Examineelement()peek()

The differences are precise, and worth learning once:

  • add(): Inserts the element at the tail. On a capacity-restricted queue that is already full, it throws an IllegalStateException.
  • offer(): Inserts the element at the tail, returning false instead of throwing when the queue cannot accept it.
  • remove(): Removes and returns the element at the head, throwing NoSuchElementException if the queue is empty.
  • poll(): Removes and returns the element at the head, returning null if the queue is empty.
  • element(): Returns the element at the head without removing it, throwing NoSuchElementException if the queue is empty.
  • peek(): Returns the element at the head without removing it, returning null if the queue is empty.

On an unbounded queue, add() and offer() behave identically, so the choice is stylistic. On a bounded queue, or anywhere several threads are involved, the offer()/poll()/peek() form is usually the better one: a full or empty queue is an ordinary condition in concurrent code, not an exceptional one, and a return value is cheaper to handle than an exception.

This is also why most queue implementations reject null elements. Since poll() and peek() use null to mean "the queue is empty," storing a real null would make the two cases indistinguishable. LinkedList is the exception that permits nulls, which is one reason it is a poor default choice as a queue.

Everything else comes from Collection: size(), isEmpty(), contains(), clear(), iterator() and stream() are all available. Note that iteration does not always follow queue order — PriorityQueue, for instance, only guarantees that the head is correct.

Java queue implementations

Because java.util.Queue is an interface, it cannot be instantiated directly. The JDK ships a number of concrete Java queue implementations, split between java.util and the concurrency utilities in java.util.concurrent:

ClassBoundedThread-safeOrderingUse when
ArrayDequeNoNoFIFOThe default single-threaded choice
LinkedListNoNoFIFOYou also need indexed access or null elements
PriorityQueueNoNoPriorityOrder should follow importance, not arrival
ConcurrentLinkedQueueNoYesFIFOMany threads, and consumers must never block
ArrayBlockingQueueYesYesFIFOFixed capacity with backpressure
LinkedBlockingQueueOptionalYesFIFOProducer-consumer handoff; always set a capacity
PriorityBlockingQueueNoYesPriorityPriority ordering with blocking consumers
DelayQueueNoYesDelay expiryElements become available at a set time
SynchronousQueueZero capacityYesDirect handoffHanding work straight to a waiting thread
LinkedTransferQueueNoYesFIFOThe producer should wait until work is picked up
LinkedBlockingDequeOptionalYesBoth endsWork stealing, or blocking access at either end

When nothing else in the list applies, ArrayDeque is the right default Java queue. It is backed by a resizable circular array, allocates nothing per element, and is generally faster than LinkedList when used as a queue.

One class that appears in search results but is not a candidate here is AbstractQueue. It is a skeletal base class that library authors extend when writing a new queue, not something application code instantiates.

Creating and using a Java queue

Declare the variable as Queue rather than as the concrete class, so the implementation can be swapped later without touching the calling code:

Queue<String> queue = new ArrayDeque<>();

queue.offer("first");
queue.offer("second");
queue.offer("third");

String head = queue.peek();    // "first" — still in the queue
String taken = queue.poll();   // "first" — now removed
int remaining = queue.size();  // 2

// drain the rest
String next;
while ((next = queue.poll()) != null) {
    process(next);
}

The drain loop above is idiomatic precisely because ArrayDeque forbids null elements, so a null return can only mean the queue is empty.

Java queue time complexity

OperationArrayDequeLinkedListPriorityQueueConcurrentLinkedQueue
offer()Amortized O(1)O(1)O(log n)O(1)
poll()O(1)O(1)O(log n)O(1)
peek()O(1)O(1)O(1)O(1)
contains()O(n)O(n)O(n)O(n)
size()O(1)O(1)O(1)O(n)

The last cell is the one that surprises people. ConcurrentLinkedQueue keeps no element counter, because maintaining one would reintroduce the contention the lock-free design exists to avoid. Its size() walks the entire queue and may be out of date by the time it returns, so it should never appear in a hot path or a loop condition.

Are Java queues thread-safe?

ArrayDeque, LinkedList and PriorityQueue are not thread-safe. Two threads polling the same instance can both receive the same element, or corrupt its internal state outright. Wrapping one in Collections.synchronizedCollection() makes individual method calls atomic but does not help with compound operations, and gives consumers nothing to wait on when the queue is empty.

The concurrency utilities offer two better answers, and which one you want depends on what an empty queue should mean:

  • Never block. ConcurrentLinkedQueue is lock-free, backed by compare-and-swap rather than locks. poll() returns null immediately when the queue is empty, which suits a consumer that has other work to get on with — but turns into a busy-wait if the consumer has nothing else to do.
  • Block until there is work. A blocking queue does the waiting for you. The BlockingQueue family adds put() and take(), which wait for space and for an element respectively. A consumer calling take() sleeps until something arrives, costing no CPU, and a producer calling put() on a full bounded queue is throttled rather than allowed to exhaust the heap. This is the standard shape for producer-consumer work, and it is what thread pools use internally.

Whichever you choose, thread safety here means safety within one JVM. None of these classes helps when the producers and consumers are separate processes on separate machines.

Queues, deques, and stacks

A queue is open at both ends but in fixed roles: elements enter at the tail and leave at the head. A deque (double-ended queue) allows insertion and removal at either end, which makes it a superset — every deque can act as a queue, which is exactly how ArrayDeque and LinkedList implement the Queue interface.

Because a deque can also push and pop at a single end, it doubles as a stack. Modern Java code uses ArrayDeque for LIFO work rather than the legacy java.util.Stack, which extends Vector and synchronizes every method whether or not you need it.

Java queues in Valkey and Redis

A java.util.Queue lives in the heap of a single JVM. That has two consequences as soon as an application grows beyond one instance. Each instance holds its own separate queue, so work placed on one is invisible to the others; and everything still queued is lost when the process restarts or crashes.

Valkey and Redis are in-memory data structure stores commonly used for key-value databases, caches, and message brokers. Neither has a dedicated queue type, but the list type serves the purpose: RPUSH appends at the tail, LPOP removes from the head, and BLPOP blocks a consumer until an element is available. Building on those commands directly leaves you to hand-roll serialization, reconnection handling, and blocking consumers yourself.

Redisson removes that work by exposing the queue as the Java interface you already use, storing the contents in Valkey or Redis so that every application instance sees the same queue:

RQueue<Order> queue = redisson.getQueue("orders");

queue.add(new Order("ORD-123"));

Order head = queue.peek();   // ORD-123, still in the queue
Order taken = queue.poll();  // ORD-123, now removed

Because RQueue extends java.util.Queue, the calling code is unchanged from the in-memory version. Switching to a blocking consumer is equally familiar:

RBlockingQueue<Order> queue = redisson.getBlockingQueue("orders");

queue.offer(new Order("ORD-124"));

Order taken = queue.poll(10, TimeUnit.MINUTES);

The poll(), pollFromAny(), pollLastAndOfferFirstTo() and take() methods resubscribe automatically during reconnection to the server or failover, so a blocked consumer survives a topology change without special handling.

Redisson provides a queue object for each of the standard Java interfaces:

Redisson objectJava interfaceUse when
RQueueQueuePlain FIFO ordering is all you need
RDequeDequeYou need access at both ends
RBlockingQueueBlockingQueueConsumers should wait for work to arrive
RBlockingDequeBlockingDequeBlocking consumers, both ends
RPriorityQueueQueueOrdering follows a comparator
RPriorityBlockingQueueBlockingQueuePriority ordering with blocking consumers
RTransferQueueTransferQueueProducers wait until work is picked up
RRingBufferQueueFixed capacity, oldest elements overwritten

Configuration details for each are covered in the Redisson queues documentation.

One limitation applies to all of them, and it is inherited from the underlying list rather than from Redisson: a popped element is gone. If the consumer crashes midway through processing it, nothing brings it back. That is acceptable for work that can be safely lost and unacceptable for orders, payments, or emails.

The Reliable Queue in Redisson PRO closes that gap with acknowledgments, visibility timeouts, delivery limits, message delay, priorities, and a dead-letter queue for messages that exhaust their retries:

RReliableQueue queue = redisson.getReliableQueue("orders");

Message msg = queue.add(QueueAddArgs.messages(
    MessageArgs.payload(new Order("ORD-125"))
));

See All about Reliable Queue for Valkey and Redis for a full walkthrough, or Redis Queue for the store-level view of how queues are built on lists.

Frequently asked questions

What is the difference between poll() and remove() in Java?

Both remove and return the element at the head of the queue. They differ only when the queue is empty: poll() returns null, while remove() throws a NoSuchElementException. The same distinction applies to offer() versus add() and to peek() versus element().

Which Java queue implementation should I use?

Use ArrayDeque for single-threaded FIFO work. Use a LinkedBlockingQueue or ArrayBlockingQueue with an explicit capacity for producer-consumer handoff between threads, and ConcurrentLinkedQueue when consumers must never block. Use PriorityQueue when order should follow importance rather than arrival.

Is java.util.Queue thread-safe?

The interface makes no such guarantee, and it depends entirely on the implementation. ArrayDeque, LinkedList and PriorityQueue are not thread-safe. The implementations in java.util.concurrentConcurrentLinkedQueue and the BlockingQueue classes — are.

Can a Java queue contain null elements?

Most implementations reject them, because poll() and peek() return null to signal an empty queue and a stored null would be ambiguous. LinkedList permits nulls, which is a good reason to prefer ArrayDeque when you need a queue.

What is the difference between a queue and a deque?

A queue accepts elements at the tail and removes them from the head. A deque allows both operations at either end, so it can act as a queue or as a stack. ArrayDeque and LinkedList are deques that also implement Queue.

Can a Java queue be shared across multiple servers?

Not with the standard classes, which exist only in the heap memory of one JVM. Storing the queue in Valkey or Redis gives every application instance a single shared view of the pending work, and keeps that work alive across restarts.

Similar terms