Mastering Java's Blocking Queues: When to Use Each JUC Queue

This article categorizes the tools in java.util.concurrent, explains the six major groups of JUC utilities, dives deep into the design and behavior of various blocking and non‑blocking queues, shows their core methods, typical use‑cases, and provides code examples for practical implementation.

Programmer DD
Programmer DD
Programmer DD
Mastering Java's Blocking Queues: When to Use Each JUC Queue

Introduction

If we roughly classify JUC utilities by purpose and characteristics, the tools can be divided into six categories: executors and thread pools, concurrent queues, synchronization tools, concurrent collections, locks, and atomic variables.

In the concurrency series , we mainly covered executors and thread pools, synchronization tools, and locks. While analyzing source code we also touched on queues, which appear in many forms in JUC. This article gives a high‑level view to quickly understand and differentiate these seemingly chaotic queues.

Concurrent Queues

Java concurrent queues can be divided into two implementation types:

Blocking queues

Non‑blocking queues

The former is based on lock implementation, the latter on CAS non‑blocking algorithms.

Common queues include the following:

Why are there so many kinds of queues? Because, like locks, queues are designed to handle different scenarios, reflecting the Single Responsibility Principle.

Blocking Queues

Blocking queues support two extra operations compared to non‑blocking queues:

Blocking insertion – when the queue is full, the inserting thread blocks until space becomes available.

Blocking removal – when the queue is empty, the removing thread blocks until an element appears.

These behaviors can be summarized in a table.

ArrayBlockingQueue

The name indicates an array‑based implementation; it can be bounded if a capacity is specified in the constructor.

public ArrayBlockingQueue(int capacity, boolean fair) {
    if (capacity <= 0)
        throw new IllegalArgumentException();
    this.items = new Object[capacity];
    lock = new ReentrantLock(fair);
    notEmpty = lock.newCondition();
    notFull = lock.newCondition();
}

By default it uses a non‑fair lock. Interview question: why is the default non‑fair lock, what are its advantages and possible drawbacks?

LinkedBlockingQueue

This is a bounded blocking queue whose default (and maximum) capacity is Integer.MAX_VALUE, making it optionally‑bounded.

public LinkedBlockingQueue() {
    this(Integer.MAX_VALUE);
}

public LinkedBlockingQueue(int capacity) {
    if (capacity <= 0) throw new IllegalArgumentException();
    this.capacity = capacity;
    last = head = new Node<E>(null);
}
Array‑based queues often have more predictable performance, while linked queues offer higher insertion/removal efficiency.

PriorityBlockingQueue

An unbounded blocking queue that orders elements according to their natural order or a supplied Comparator.

Null elements are not allowed; elements must be comparable.

Elements with equal priority have undefined ordering unless a secondary rule is provided.

Because it is unbounded, put never blocks; it simply delegates to offer.

public void put(E e) {
    offer(e); // never blocks
}

The default initial capacity is 11 and grows automatically.

DelayQueue

A unbounded blocking queue that holds elements implementing Delayed. Elements become available after their delay expires.

public long getDelay(TimeUnit unit) {
    return unit.convert(time - now(), NANOSECONDS);
}

public int compareTo(Delayed other) {
    if (other == this) return 0;
    long diff = getDelay(NANOSECONDS) - other.getDelay(NANOSECONDS);
    return (diff == 0) ? 0 : (diff < 0 ? -1 : 1);
}

Typical use‑cases: cache expiration and scheduled task execution.

SynchronousQueue

A queue that does not store elements; each put must wait for a corresponding take and vice versa.

ExecutorService executor = Executors.newFixedThreadPool(2);
SynchronousQueue<Integer> queue = new SynchronousQueue<>();

Runnable producer = () -> {
    Integer e = ThreadLocalRandom.current().nextInt();
    try { queue.put(e); } catch (InterruptedException ex) { ex.printStackTrace(); }
};

Runnable consumer = () -> {
    try { Integer v = queue.take(); } catch (InterruptedException ex) { ex.printStackTrace(); }
};
Executors.newCachedThreadPool()

internally uses a SynchronousQueue because the pool can grow to Integer.MAX_VALUE threads, so tasks are handed off directly without queuing.

LinkedTransferQueue

Provides a transfer method that blocks until the element is consumed. It also offers tryTransfer (non‑blocking) and tryTransfer with timeout.

BlockingQueue blocks when the queue is full; TransferQueue blocks when there is no consumer waiting.

LinkedBlockingDeque

A double‑ended blocking queue based on a linked list, allowing insertion and removal at both head and tail, which reduces contention in concurrent scenarios.

Dual‑ended queues provide an extra entry point, halving contention when multiple threads enqueue simultaneously.

Summary

This article quickly categorizes Java's blocking queues, clarifies their design differences, and explains when to use each. Understanding these queues helps you read source code with confidence and choose the right implementation for your concurrency needs.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

ThreadPoolJUCBlockingQueue
Programmer DD
Written by

Programmer DD

A tinkering programmer and author of "Spring Cloud Microservices in Action"

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.