| Implementation | Capacity | Ordering | Best for |
LinkedBlockingQueue | Optionally bounded (default: Integer.MAX_VALUE) | FIFO | General-purpose; high throughput (separate head/tail locks) |
ArrayBlockingQueue | Fixed at construction | FIFO | Bounded buffers; predictable memory; fair option |
SynchronousQueue | Zero (no internal storage) | Direct handoff | Thread-pool task handoff; rendezvous pattern |
PriorityBlockingQueue | Unbounded | Priority (natural/Comparator) | Priority task queues; scheduled work |
DelayQueue | Unbounded | Delay expiry | Scheduled tasks, retry with backoff, TTL eviction |
LinkedTransferQueue | Unbounded | FIFO | Highest throughput; transfer() for synchronous hand-off |
| Operation | Throws | Returns special value | Blocks | Times out |
| Insert | add(e) | offer(e) | put(e) | offer(e, t, u) |
| Remove | remove() | poll() | take() | poll(t, u) |
| Examine | element() | peek() | — | — |
import java.util.concurrent.*;
BlockingQueue<String> queue = new LinkedBlockingQueue<>(10);
// Blocking put — waits if queue is full
queue.put("task-1");
// Blocking take — waits if queue is empty
String item = queue.take();
// Non-blocking — returns false/null immediately
boolean enqueued = queue.offer("task-2");
String polled = queue.poll();
// Timed — waits up to given duration
boolean ok = queue.offer("task-3", 500, TimeUnit.MILLISECONDS);
String got = queue.poll(1, TimeUnit.SECONDS); // null on timeout
import java.util.concurrent.*;
public class ProducerConsumer {
private static final String POISON_PILL = "__DONE__";
public static void main(String[] args) throws InterruptedException {
BlockingQueue<String> queue = new LinkedBlockingQueue<>(50);
int consumerCount = 3;
// Producer thread
Thread producer = new Thread(() -> {
try {
for (int i = 1; i <= 20; i++) {
queue.put("task-" + i);
System.out.println("Produced: task-" + i);
}
// Send one poison pill per consumer
for (int i = 0; i < consumerCount; i++) {
queue.put(POISON_PILL);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
// Consumer threads
ExecutorService consumers = Executors.newFixedThreadPool(consumerCount);
for (int i = 0; i < consumerCount; i++) {
final int id = i;
consumers.submit(() -> {
try {
while (true) {
String task = queue.take();
if (POISON_PILL.equals(task)) break;
System.out.println("Consumer " + id + " processing: " + task);
Thread.sleep(50); // simulate work
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
producer.start();
producer.join();
consumers.shutdown();
consumers.awaitTermination(10, TimeUnit.SECONDS);
}
}
The poison-pill pattern is the cleanest way to signal consumer shutdown: put one sentinel value per consumer thread. Each consumer exits when it receives the pill, without any shared flag or volatile boolean.
ArrayBlockingQueue uses a single lock for both head and tail operations (unlike LinkedBlockingQueue which uses two). This makes it slightly lower throughput but with an optional fair mode that prevents thread starvation.
// Bounded to 100 elements — rejects (blocks) when full
ArrayBlockingQueue<Integer> bounded = new ArrayBlockingQueue<>(100);
// Fair mode — longest-waiting thread gets the slot first
ArrayBlockingQueue<Integer> fair = new ArrayBlockingQueue<>(100, true);
// Drain all elements into a list (non-blocking bulk operation)
List<Integer> batch = new ArrayList<>();
int drained = bounded.drainTo(batch, 10); // drain up to 10 items
System.out.println("Drained: " + drained + " items");
SynchronousQueue has no internal buffer. A put() blocks until another thread calls take(), and vice versa. It is used internally by Executors.newCachedThreadPool() for immediate task-to-thread hand-off.
import java.util.concurrent.SynchronousQueue;
SynchronousQueue<String> handoff = new SynchronousQueue<>();
// Producer — blocks until a consumer is ready
Thread producer = new Thread(() -> {
try {
System.out.println("Producer waiting for consumer...");
handoff.put("Hello!"); // blocks here
System.out.println("Handed off");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
// Consumer — blocks until producer offers
Thread consumer = new Thread(() -> {
try {
String msg = handoff.take(); // blocks here
System.out.println("Received: " + msg);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
producer.start();
Thread.sleep(100);
consumer.start();
import java.util.concurrent.PriorityBlockingQueue;
record Task(int priority, String name) implements Comparable<Task> {
@Override
public int compareTo(Task other) {
return Integer.compare(other.priority, this.priority); // higher priority first
}
}
PriorityBlockingQueue<Task> pq = new PriorityBlockingQueue<>();
pq.put(new Task(1, "Low priority"));
pq.put(new Task(10, "Critical"));
pq.put(new Task(5, "Medium"));
System.out.println(pq.take().name()); // Critical (priority 10)
System.out.println(pq.take().name()); // Medium (priority 5)
System.out.println(pq.take().name()); // Low priority (priority 1)
PriorityBlockingQueue is unbounded — it never blocks producers. A fast producer and slow consumer can cause unbounded memory growth. Consider using a bounded ArrayBlockingQueue with a custom comparator through a TreeSet-backed wrapper if you need both priority and bounding.
DelayQueue only releases elements whose delay has expired. Elements must implement java.util.concurrent.Delayed.
import java.util.concurrent.*;
class DelayedTask implements Delayed {
private final String name;
private final long runAt; // absolute nanotime
DelayedTask(String name, long delayMs) {
this.name = name;
this.runAt = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(delayMs);
}
@Override
public long getDelay(TimeUnit unit) {
return unit.convert(runAt - System.nanoTime(), TimeUnit.NANOSECONDS);
}
@Override
public int compareTo(Delayed other) {
return Long.compare(
getDelay(TimeUnit.NANOSECONDS),
other.getDelay(TimeUnit.NANOSECONDS));
}
public String getName() { return name; }
}
public class DelayQueueExample {
public static void main(String[] args) throws InterruptedException {
DelayQueue<DelayedTask> queue = new DelayQueue<>();
queue.put(new DelayedTask("task-A", 2000)); // runs after 2 s
queue.put(new DelayedTask("task-B", 500)); // runs after 0.5 s
queue.put(new DelayedTask("task-C", 1000)); // runs after 1 s
while (!queue.isEmpty()) {
DelayedTask t = queue.take(); // blocks until next task is ready
System.out.println("Running: " + t.getName());
}
// Output order: task-B, task-C, task-A
}
}