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:
| Operation | Throws an exception | Returns a special value |
|---|---|---|
| Insert | add(e) | offer(e) |
| Remove | remove() | poll() |
| Examine | element() | 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
falseinstead of throwing when the queue cannot accept it. - remove(): Removes and returns the element at the head, throwing
NoSuchElementExceptionif the queue is empty. - poll(): Removes and returns the element at the head, returning
nullif the queue is empty. - element(): Returns the element at the head without removing it, throwing
NoSuchElementExceptionif the queue is empty. - peek(): Returns the element at the head without removing it, returning
nullif 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:
| Class | Bounded | Thread-safe | Ordering | Use when |
|---|---|---|---|---|
ArrayDeque | No | No | FIFO | The default single-threaded choice |
LinkedList | No | No | FIFO | You also need indexed access or null elements |
PriorityQueue | No | No | Priority | Order should follow importance, not arrival |
ConcurrentLinkedQueue | No | Yes | FIFO | Many threads, and consumers must never block |
ArrayBlockingQueue | Yes | Yes | FIFO | Fixed capacity with backpressure |
LinkedBlockingQueue | Optional | Yes | FIFO | Producer-consumer handoff; always set a capacity |
PriorityBlockingQueue | No | Yes | Priority | Priority ordering with blocking consumers |
DelayQueue | No | Yes | Delay expiry | Elements become available at a set time |
SynchronousQueue | Zero capacity | Yes | Direct handoff | Handing work straight to a waiting thread |
LinkedTransferQueue | No | Yes | FIFO | The producer should wait until work is picked up |
LinkedBlockingDeque | Optional | Yes | Both ends | Work 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
| Operation | ArrayDeque | LinkedList | PriorityQueue | ConcurrentLinkedQueue |
|---|---|---|---|---|
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.
ConcurrentLinkedQueueis lock-free, backed by compare-and-swap rather than locks.poll()returnsnullimmediately 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()andtake(), which wait for space and for an element respectively. A consumer callingtake()sleeps until something arrives, costing no CPU, and a producer callingput()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 object | Java interface | Use when |
|---|---|---|
RQueue | Queue | Plain FIFO ordering is all you need |
RDeque | Deque | You need access at both ends |
RBlockingQueue | BlockingQueue | Consumers should wait for work to arrive |
RBlockingDeque | BlockingDeque | Blocking consumers, both ends |
RPriorityQueue | Queue | Ordering follows a comparator |
RPriorityBlockingQueue | BlockingQueue | Priority ordering with blocking consumers |
RTransferQueue | TransferQueue | Producers wait until work is picked up |
RRingBuffer | Queue | Fixed 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.concurrent — ConcurrentLinkedQueue 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.