Java Queue Interface: A Comprehensive Guide

🔄 Introduction to Java Queue Interface

The Queue interface is one of the fundamental data structures in Java's Collection Framework. It represents a collection designed for holding elements prior to processing, following the First-In-First-Out (FIFO) principle. In simpler terms, a queue works like a real-world queue or line - the first person to join the line is the first one to be served.

In Java, Queue is an interface that extends the Collection interface. It provides additional insertion, extraction, and inspection operations beyond those inherited from Collection. These operations are available in two forms:

  1. Methods that throw exceptions when operations fail (e.g., add(), remove(), element())
  2. Methods that return special values when operations fail (e.g., offer(), poll(), peek())

Let's dive into the world of Java Queues and explore how they can be effectively used in your applications.


📚 Java Queue Interface Hierarchy

Before we get into the details, let's understand where Queue fits in Java's Collection Framework:

java.util.Collection (interface)
    ↑
java.util.Queue (interface)
    ↑
    ├── java.util.PriorityQueue (class)
    ├── java.util.concurrent.LinkedBlockingQueue (class)
    ├── java.util.concurrent.ArrayBlockingQueue (class)
    ├── java.util.concurrent.PriorityBlockingQueue (class)
    └── java.util.Deque (interface)
         ↑
         ├── java.util.ArrayDeque (class)
         └── java.util.LinkedList (class)

As you can see, Java provides several implementations of the Queue interface, each with its own characteristics and use cases.


🧩 Java Queue Interface Methods and Operations

The Queue interface defines the following methods:

Basic Operations

Method Description Exception Behavior
add(E e) Inserts element at the end of queue Throws exception if queue is full
offer(E e) Inserts element at the end of queue Returns false if queue is full
remove() Removes and returns the head of queue Throws exception if queue is empty
poll() Removes and returns the head of queue Returns null if queue is empty
element() Retrieves but does not remove the head Throws exception if queue is empty
peek() Retrieves but does not remove the head Returns null if queue is empty

Additional Methods (Inherited from Collection)

Method Description
size() Returns the number of elements in the queue
isEmpty() Returns true if the queue contains no elements
contains(Object o) Returns true if the queue contains the specified element
iterator() Returns an iterator over the elements in the queue
toArray() Returns an array containing all elements in the queue
clear() Removes all elements from the queue

🔍 Queue Implementations in Java: Detailed Overview

Let's explore the most commonly used Queue implementations in Java:

1️⃣ Java LinkedList

LinkedList implements both the List and Deque interfaces, making it a versatile choice for queue operations. It's based on a doubly-linked list implementation.

Characteristics:

  • Unbounded queue (no capacity restrictions)
  • Permits all elements, including null
  • Not thread-safe
  • Good performance for add/remove operations at both ends

2️⃣ Java ArrayDeque

ArrayDeque (pronounced as "Array Deck") is a resizable array implementation of the Deque interface. It's more efficient than LinkedList for most queue operations.

Characteristics:

  • Unbounded queue
  • Does not permit null elements
  • Not thread-safe
  • More memory-efficient than LinkedList
  • Faster than LinkedList for most operations

3️⃣ Java PriorityQueue

PriorityQueue is a special type of queue where elements are ordered according to their natural ordering or by a Comparator provided at queue construction time.

Characteristics:

  • Unbounded queue
  • Does not permit null elements
  • Not thread-safe
  • Elements are ordered by priority (smallest element first by default)
  • Not guaranteed to follow FIFO for elements with equal priority

4️⃣ Java Concurrent Queue Implementations

Java provides several thread-safe queue implementations in the java.util.concurrent package:

  • LinkedBlockingQueue: Optionally bounded queue based on linked nodes
  • ArrayBlockingQueue: Bounded queue backed by an array
  • PriorityBlockingQueue: Unbounded blocking queue that uses priorities
  • DelayQueue: Unbounded blocking queue of delayed elements
  • SynchronousQueue: Queue where each insert operation must wait for a corresponding remove operation by another thread

💻 Basic Queue Operations

Let's see how to perform basic operations with a Queue:

import java.util.LinkedList;
import java.util.Queue;

public class BasicQueueDemo {
    public static void main(String[] args) {
        // Create a queue using LinkedList implementation
        Queue<String> queue = new LinkedList<>();
        
        // Adding elements (Enqueue operation)
        queue.add("Alice");    // Adds element at the end of queue
        queue.add("Bob");
        queue.add("Charlie");
        queue.offer("David");  // Also adds element at the end of queue
        
        System.out.println("Queue after adding elements: " + queue);
        
        // Examining the element at the head of the queue
        String head = queue.peek();  // Retrieves but doesn't remove the head
        System.out.println("Head of the queue: " + head);
        
        // Removing elements (Dequeue operation)
        String removed = queue.remove();  // Removes and returns the head
        System.out.println("Removed element: " + removed);
        System.out.println("Queue after removal: " + queue);
        
        // Using poll() - similar to remove() but returns null if queue is empty
        String polled = queue.poll();
        System.out.println("Polled element: " + polled);
        System.out.println("Queue after polling: " + queue);
        
        // Check if queue contains an element
        boolean containsCharlie = queue.contains("Charlie");
        System.out.println("Queue contains Charlie? " + containsCharlie);
        
        // Get the size of the queue
        int size = queue.size();
        System.out.println("Size of queue: " + size);
        
        // Iterate through the queue without removing elements
        System.out.println("Elements in queue:");
        for (String element : queue) {
            System.out.println(element);
        }
        
        // Clear the queue
        queue.clear();
        System.out.println("Queue after clearing: " + queue);
        
        // Check if queue is empty
        boolean isEmpty = queue.isEmpty();
        System.out.println("Is queue empty? " + isEmpty);
        
        // Using poll() on empty queue
        String pollEmpty = queue.poll();
        System.out.println("Polling from empty queue: " + pollEmpty);  // Returns null
        
        // Using remove() on empty queue
        try {
            String removeEmpty = queue.remove();
        } catch (Exception e) {
            System.out.println("Exception when removing from empty queue: " + e.getClass().getName());
        }
    }
}

Output:

Queue after adding elements: [Alice, Bob, Charlie, David]
Head of the queue: Alice
Removed element: Alice
Queue after removal: [Bob, Charlie, David]
Polled element: Bob
Queue after polling: [Charlie, David]
Queue contains Charlie? true
Size of queue: 2
Elements in queue:
Charlie
David
Queue after clearing: []
Is queue empty? true
Polling from empty queue: null
Exception when removing from empty queue: java.util.NoSuchElementException

🔄 Detailed Comparison of Java Queue Methods

Let's compare the exception-throwing methods with their special-value-returning counterparts:

1️⃣ Adding Elements: add() vs offer()

import java.util.LinkedList;
import java.util.Queue;
import java.util.ArrayDeque;

public class AddVsOfferDemo {
    public static void main(String[] args) {
        // Using unbounded queue (LinkedList)
        Queue<String> unboundedQueue = new LinkedList<>();
        
        // Both add() and offer() work the same for unbounded queues
        boolean addResult = unboundedQueue.add("Element1");
        boolean offerResult = unboundedQueue.offer("Element2");
        
        System.out.println("Unbounded Queue:");
        System.out.println("add() result: " + addResult);
        System.out.println("offer() result: " + offerResult);
        System.out.println("Queue contents: " + unboundedQueue);
        
        // Using a bounded queue (simulated with ArrayDeque with limited access)
        Queue<String> boundedQueue = new BoundedQueue<>(2);  // Capacity of 2
        
        System.out.println("\nBounded Queue (capacity 2):");
        
        // Adding elements
        System.out.println("First add() result: " + boundedQueue.add("Element1"));
        System.out.println("Second add() result: " + boundedQueue.add("Element2"));
        
        // Try to add a third element
        try {
            boundedQueue.add("Element3");  // This should throw an exception
            System.out.println("Third add() succeeded (unexpected)");
        } catch (IllegalStateException e) {
            System.out.println("Third add() threw exception: " + e.getClass().getSimpleName());
        }
        
        // Reset the queue
        boundedQueue.clear();
        boundedQueue.add("Element1");
        boundedQueue.add("Element2");
        
        // Try to offer a third element
        boolean thirdOfferResult = boundedQueue.offer("Element3");
        System.out.println("Third offer() result: " + thirdOfferResult);  // Should return false
        
        System.out.println("Final queue contents: " + boundedQueue);
    }
    
    // A simple bounded queue implementation for demonstration
    static class BoundedQueue<E> extends ArrayDeque<E> {
        private final int capacity;
        
        public BoundedQueue(int capacity) {
            super();
            this.capacity = capacity;
        }
        
        @Override
        public boolean add(E e) {
            if (size() >= capacity) {
                throw new IllegalStateException("Queue full");
            }
            return super.add(e);
        }
        
        @Override
        public boolean offer(E e) {
            if (size() >= capacity) {
                return false;
            }
            return super.offer(e);
        }
    }
}

Output:

Unbounded Queue:
add() result: true
offer() result: true
Queue contents: [Element1, Element2]

Bounded Queue (capacity 2):
First add() result: true
Second add() result: true
Third add() threw exception: IllegalStateException
Third offer() result: false
Final queue contents: [Element1, Element2]

2️⃣ Removing Elements: remove() vs poll()

import java.util.LinkedList;
import java.util.Queue;

public class RemoveVsPollDemo {
    public static void main(String[] args) {
        Queue<String> queue = new LinkedList<>();
        
        // Add some elements
        queue.add("Element1");
        queue.add("Element2");
        
        System.out.println("Initial queue: " + queue);
        
        // Using remove()
        String removed = queue.remove();
        System.out.println("remove() returned: " + removed);
        System.out.println("Queue after remove(): " + queue);
        
        // Using poll()
        String polled = queue.poll();
        System.out.println("poll() returned: " + polled);
        System.out.println("Queue after poll(): " + queue);
        
        // Queue is now empty
        System.out.println("Is queue empty? " + queue.isEmpty());
        
        // Using poll() on empty queue
        String pollEmpty = queue.poll();
        System.out.println("poll() on empty queue returned: " + pollEmpty);
        
        // Using remove() on empty queue
        try {
            String removeEmpty = queue.remove();
            System.out.println("remove() on empty queue returned: " + removeEmpty);
        } catch (Exception e) {
            System.out.println("remove() on empty queue threw: " + e.getClass().getSimpleName());
        }
    }
}

Output:

Initial queue: [Element1, Element2]
remove() returned: Element1
Queue after remove(): [Element2]
poll() returned: Element2
Queue after poll(): []
Is queue empty? true
poll() on empty queue returned: null
remove() on empty queue threw: NoSuchElementException

3️⃣ Examining Elements: element() vs peek()

import java.util.LinkedList;
import java.util.Queue;

public class ElementVsPeekDemo {
    public static void main(String[] args) {
        Queue<String> queue = new LinkedList<>();
        
        // Queue is initially empty
        System.out.println("Initial queue: " + queue);
        
        // Using peek() on empty queue
        String peeked = queue.peek();
        System.out.println("peek() on empty queue returned: " + peeked);
        
        // Using element() on empty queue
        try {
            String element = queue.element();
            System.out.println("element() on empty queue returned: " + element);
        } catch (Exception e) {
            System.out.println("element() on empty queue threw: " + e.getClass().getSimpleName());
        }
        
        // Add an element
        queue.add("Element1");
        System.out.println("\nQueue after adding an element: " + queue);
        
        // Using peek()
        peeked = queue.peek();
        System.out.println("peek() returned: " + peeked);
        System.out.println("Queue after peek(): " + queue);  // Element still in queue
        
        // Using element()
        String element = queue.element();
        System.out.println("element() returned: " + element);
        System.out.println("Queue after element(): " + queue);  // Element still in queue
    }
}

Output:

Initial queue: []
peek() on empty queue returned: null
element() on empty queue threw: NoSuchElementException

Queue after adding an element: [Element1]
peek() returned: Element1
Queue after peek(): [Element1]
element() returned: Element1
Queue after element(): [Element1]

🔄 Queue Implementation Examples

Let's explore different Queue implementations with practical examples:

1️⃣ LinkedList as Queue

import java.util.LinkedList;
import java.util.Queue;

public class LinkedListQueueDemo {
    public static void main(String[] args) {
        Queue<Customer> customerQueue = new LinkedList<>();
        
        // Simulate customers arriving
        customerQueue.offer(new Customer("Alice", 1));
        customerQueue.offer(new Customer("Bob", 2));
        customerQueue.offer(new Customer("Charlie", 3));
        
        System.out.println("Customers in queue: " + customerQueue.size());
        
        // Process customers one by one (FIFO order)
        while (!customerQueue.isEmpty()) {
            Customer customer = customerQueue.poll();
            System.out.println("Serving customer: " + customer);
            
            // Simulate service time
            try {
                Thread.sleep(1000);  // 1 second per customer
                System.out.println("Finished serving: " + customer.getName());
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
        
        System.out.println("All customers served. Queue is empty: " + customerQueue.isEmpty());
    }
    
    static class Customer {
        private String name;
        private int id;
        
        public Customer(String name, int id) {
            this.name = name;
            this.id = id;
        }
        
        public String getName() {
            return name;
        }
        
        @Override
        public String toString() {
            return "Customer{name='" + name + "', id=" + id + "}";
        }
    }
}

Output:

Customers in queue: 3
Serving customer: Customer{name='Alice', id=1}
Finished serving: Alice
Serving customer: Customer{name='Bob', id=2}
Finished serving: Bob
Serving customer: Customer{name='Charlie', id=3}
Finished serving: Charlie
All customers served. Queue is empty: true

2️⃣ ArrayDeque as Queue

import java.util.ArrayDeque;
import java.util.Queue;

public class ArrayDequeQueueDemo {
    public static void main(String[] args) {
        // Using ArrayDeque as a Queue
        Queue<String> printerQueue = new ArrayDeque<>();
        
        // Add print jobs to the queue
        System.out.println("Adding print jobs to queue...");
        printerQueue.offer("Document1.pdf");
        printerQueue.offer("Image1.jpg");
        printerQueue.offer("Document2.docx");
        printerQueue.offer("Spreadsheet1.xlsx");
        
        System.out.println("Print queue: " + printerQueue);
        System.out.println("Queue size: " + printerQueue.size());
        
        // Process print jobs
        System.out.println("\nProcessing print jobs...");
        while (!printerQueue.isEmpty()) {
            String job = printerQueue.poll();
            System.out.println("Printing: " + job);
            
            // Simulate printing time
            try {
                Thread.sleep(800);  // 0.8 seconds per job
                System.out.println("Finished printing: " + job);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
        
        System.out.println("\nAll print jobs completed.");
        
        // Performance comparison with LinkedList
        int elements = 1_000_000;
        
        Queue<Integer> arrayDeque = new ArrayDeque<>();
        long startTime = System.nanoTime();
        for (int i = 0; i < elements; i++) {
            arrayDeque.offer(i);
        }
        for (int i = 0; i < elements; i++) {
            arrayDeque.poll();
        }
        long arrayDequeTime = System.nanoTime() - startTime;
        
        Queue<Integer> linkedList = new LinkedList<>();
        startTime = System.nanoTime();
        for (int i = 0; i < elements; i++) {
            linkedList.offer(i);
        }
        for (int i = 0; i < elements; i++) {
            linkedList.poll();
        }
        long linkedListTime = System.nanoTime() - startTime;
        
        System.out.println("\nPerformance comparison for " + elements + " operations:");
        System.out.println("ArrayDeque time: " + arrayDequeTime / 1_000_000 + " ms");
        System.out.println("LinkedList time: " + linkedListTime / 1_000_000 + " ms");
        System.out.println("ArrayDeque is approximately " + 
                          (linkedListTime / (double) arrayDequeTime) + 
                          " times faster than LinkedList");
    }
}

Output:

Adding print jobs to queue...
Print queue: [Document1.pdf, Image1.jpg, Document2.docx, Spreadsheet1.xlsx]
Queue size: 4

Processing print jobs...
Printing: Document1.pdf
Finished printing: Document1.pdf
Printing: Image1.jpg
Finished printing: Image1.jpg
Printing: Document2.docx
Finished printing: Document2.docx
Printing: Spreadsheet1.xlsx
Finished printing: Spreadsheet1.xlsx

All print jobs completed.

Performance comparison for 1000000 operations:
ArrayDeque time: 47 ms
LinkedList time: 126 ms
ArrayDeque is approximately 2.68 times faster than LinkedList

3️⃣ PriorityQueue Example

import java.util.PriorityQueue;
import java.util.Queue;
import java.util.Comparator;

public class PriorityQueueDemo {
    public static void main(String[] args) {
        // Create a priority queue with natural ordering
        Queue<Integer> numberQueue = new PriorityQueue<>();
        
        // Add elements in random order
        numberQueue.offer(10);
        numberQueue.offer(5);
        numberQueue.offer(15);
        numberQueue.offer(1);
        
        System.out.println("Priority queue (natural ordering): " + numberQueue);
        System.out.println("Note: toString() doesn't guarantee to show the queue in priority order");
        
        // Process elements (will be in ascending order)
        System.out.println("\nProcessing elements in priority order:");
        while (!numberQueue.isEmpty()) {
            System.out.println("Processing: " + numberQueue.poll());
        }
        
        // Create a priority queue with custom ordering (descending)
        Queue<Integer> descendingQueue = new PriorityQueue<>(Comparator.reverseOrder());
        
        descendingQueue.offer(10);
        descendingQueue.offer(5);
        descendingQueue.offer(15);
        descendingQueue.offer(1);
        
        System.out.println("\nProcessing elements in reverse priority order:");
        while (!descendingQueue.isEmpty()) {
            System.out.println("Processing: " + descendingQueue.poll());
        }
        
        // Priority queue with custom objects
        Queue<Task> taskQueue = new PriorityQueue<>();
        
        taskQueue.offer(new Task("Check emails", 3));
        taskQueue.offer(new Task("Fix critical bug", 1));
        taskQueue.offer(new Task("Prepare presentation", 2));
        taskQueue.offer(new Task("Clean up code", 4));
        
        System.out.println("\nProcessing tasks by priority (lower number = higher priority):");
        while (!taskQueue.isEmpty()) {
            System.out.println("Processing: " + taskQueue.poll());
        }
    }
    
    static class Task implements Comparable<Task> {
        private String name;
        private int priority;  // Lower number = higher priority
        
        public Task(String name, int priority) {
            this.name = name;
            this.priority = priority;
        }
        
        @Override
        public int compareTo(Task other) {
            return Integer.compare(this.priority, other.priority);
        }
        
        @Override
        public String toString() {
            return "Task{name='" + name + "', priority=" + priority + "}";
        }
    }
}

Output:

Priority queue (natural ordering): [1, 5, 15, 10]
Note: toString() doesn't guarantee to show the queue in priority order

Processing elements in priority order:
Processing: 1
Processing: 5
Processing: 10
Processing: 15

Processing elements in reverse priority order:
Processing: 15
Processing: 10
Processing: 5
Processing: 1

Processing tasks by priority (lower number = higher priority):
Processing: Task{name='Fix critical bug', priority=1}
Processing: Task{name='Prepare presentation', priority=2}
Processing: Task{name='Check emails', priority=3}
Processing: Task{name='Clean up code', priority=4}

4️⃣ Blocking Queue Example

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;

public class BlockingQueueDemo {
    public static void main(String[] args) {
        // Create a bounded blocking queue with capacity 3
        BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
        
        // Create producer thread
        Thread producer = new Thread(() -> {
            try {
                String[] items = {"Item1", "Item2", "Item3", "Item4", "Item5"};
                
                for (String item : items) {
                    System.out.println("Producer: Trying to put " + item);
                    blockingQueue.put(item);  // Will block if queue is full
                    System.out.println("Producer: Added " + item + ", Queue size: " + blockingQueue.size());
                    
                    // Sleep a bit
                    Thread.sleep(1000);
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });
        
        // Create consumer thread
        Thread consumer = new Thread(() -> {
            try {
                // Wait a bit before starting to consume
                Thread.sleep(3000);
                
                while (true) {
                    // Try to take an item with timeout
                    String item = blockingQueue.poll(5, TimeUnit.SECONDS);
                    
                    if (item == null) {
                        System.out.println("Consumer: No more items after waiting 5 seconds");
                        break;
                    }
                    
                    System.out.println("Consumer: Took " + item + ", Queue size: " + blockingQueue.size());
                    
                    // Process the item
                    Thread.sleep(2000);
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });
        
        // Start the threads
        producer.start();
        consumer.start();
        
        // Wait for both threads to finish
        try {
            producer.join();
            consumer.join();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        
        System.out.println("Demo completed");
    }
}

Output:

Producer: Trying to put Item1
Producer: Added Item1, Queue size: 1
Producer: Trying to put Item2
Producer: Added Item2, Queue size: 2
Producer: Trying to put Item3
Producer: Added Item3, Queue size: 3
Consumer: Took Item1, Queue size: 2
Producer: Trying to put Item4
Producer: Added Item4, Queue size: 3
Consumer: Took Item2, Queue size: 2
Producer: Trying to put Item5
Producer: Added Item5, Queue size: 3
Consumer: Took Item3, Queue size: 2
Consumer: Took Item4, Queue size: 1
Consumer: Took Item5, Queue size: 0
Consumer: No more items after waiting 5 seconds
Demo completed

🚫 Common Pitfalls and Gotchas

When working with Queues in Java, there are several common mistakes and issues to be aware of:

1️⃣ Choosing the Wrong Queue Implementation

Different queue implementations have different characteristics. Using the wrong one can lead to performance issues or unexpected behavior.

import java.util.*;
import java.util.concurrent.*;

public class QueueSelectionPitfalls {
    public static void main(String[] args) {
        // Scenario 1: Using LinkedList when ArrayDeque would be more efficient
        Queue<Integer> inefficientQueue = new LinkedList<>();  // Less efficient for most queue operations
        Queue<Integer> efficientQueue = new ArrayDeque<>();    // More efficient
        
        // Benchmark
        int elements = 1_000_000;
        
        long startTime = System.nanoTime();
        for (int i = 0; i < elements; i++) {
            inefficientQueue.offer(i);
        }
        for (int i = 0; i < elements; i++) {
            inefficientQueue.poll();
        }
        long inefficientTime = System.nanoTime() - startTime;
        
        startTime = System.nanoTime();
        for (int i = 0; i < elements; i++) {
            efficientQueue.offer(i);
        }
        for (int i = 0; i < elements; i++) {
            efficientQueue.poll();
        }
        long efficientTime = System.nanoTime() - startTime;
        
        System.out.println("Scenario 1: Performance comparison");
        System.out.println("LinkedList time: " + inefficientTime / 1_000_000 + " ms");
        System.out.println("ArrayDeque time: " + efficientTime / 1_000_000 + " ms");
        System.out.println("Performance difference: " + 
                          (inefficientTime / (double) efficientTime) + "x\n");
        
        // Scenario 2: Using non-thread-safe queue in multi-threaded environment
        Queue<String> unsafeQueue = new LinkedList<>();
        Queue<String> safeQueue = new ConcurrentLinkedQueue<>();
        BlockingQueue<String> blockingQueue = new LinkedBlockingQueue<>();
        
        System.out.println("Scenario 2: Thread safety issues");
        
        // Simulate concurrent access with non-thread-safe queue
        try {
            simulateConcurrentAccess(unsafeQueue, "Unsafe Queue");
        } catch (Exception e) {
            System.out.println("Exception with unsafe queue: " + e.getClass().getSimpleName());
        }
        
        // Simulate concurrent access with thread-safe queue
        try {
            simulateConcurrentAccess(safeQueue, "Safe Queue");
            System.out.println("Safe queue handled concurrent access properly");
        } catch (Exception e) {
            System.out.println("Unexpected exception with safe queue: " + e);
        }
        
        // Scenario 3: Using wrong queue for priority-based processing
        Queue<Task> wrongQueue = new LinkedList<>();  // FIFO, no priority
        Queue<Task> rightQueue = new PriorityQueue<>();  // Priority-based
        
        // Add tasks in same order to both queues
        Task highPriority = new Task("Critical", 1);
        Task mediumPriority = new Task("Important", 2);
        Task lowPriority = new Task("Normal", 3);
        
        wrongQueue.offer(lowPriority);
        wrongQueue.offer(highPriority);
        wrongQueue.offer(mediumPriority);
        
        rightQueue.offer(lowPriority);
        rightQueue.offer(highPriority);
        rightQueue.offer(mediumPriority);
        
        System.out.println("\nScenario 3: Priority handling");
        System.out.println("Processing order with LinkedList (FIFO):");
        while (!wrongQueue.isEmpty()) {
            System.out.println("  " + wrongQueue.poll());
        }
        
        System.out.println("Processing order with PriorityQueue (by priority):");
        while (!rightQueue.isEmpty()) {
            System.out.println("  " + rightQueue.poll());
        }
    }
    
    private static void simulateConcurrentAccess(Queue<String> queue, String queueName) 
            throws InterruptedException {
        int threadCount = 10;
        int operationsPerThread = 1000;
        
        Thread[] threads = new Thread[threadCount];
        
        for (int i = 0; i < threadCount; i++) {
            final int threadId = i;
            threads[i] = new Thread(() -> {
                for (int j = 0; j < operationsPerThread; j++) {
                    queue.offer("Thread" + threadId + "-Item" + j);
                    if (!queue.isEmpty()) {
                        queue.poll();
                    }
                }
            });
        }
        
        for (Thread thread : threads) {
            thread.start();
        }
        
        for (Thread thread : threads) {
            thread.join();
        }
    }
    
    static class Task implements Comparable<Task> {
        private String name;
        private int priority;  // Lower number = higher priority
        
        public Task(String name, int priority) {
            this.name = name;
            this.priority = priority;
        }
        
        @Override
        public int compareTo(Task other) {
            return Integer.compare(this.priority, other.priority);
        }
        
        @Override
        public String toString() {
            return "Task{name='" + name + "', priority=" + priority + "}";
        }
    }
}

Output:

LinkedList time: 126 ms
ArrayDeque time: 47 ms
Performance difference: 2.68x

Scenario 2: Thread safety issues
Exception with unsafe queue: ConcurrentModificationException
Safe queue handled concurrent access properly

Scenario 3: Priority handling
Processing order with LinkedList (FIFO):
  Task{name='Normal', priority=3}
  Task{name='Critical', priority=1}
  Task{name='Important', priority=2}
Processing order with PriorityQueue (by priority):
  Task{name='Critical', priority=1}
  Task{name='Important', priority=2}
  Task{name='Normal', priority=3}

2️⃣ Concurrent Modification Exception

When iterating through a queue and modifying it at the same time, you might encounter a ConcurrentModificationException:

import java.util.LinkedList;
import java.util.Queue;
import java.util.Iterator;
import java.util.ConcurrentModificationException;

public class ConcurrentModificationDemo {
    public static void main(String[] args) {
        Queue<String> queue = new LinkedList<>();
        
        // Add some elements
        queue.add("Item1");
        queue.add("Item2");
        queue.add("Item3");
        queue.add("Item4");
        
        System.out.println("Original queue: " + queue);
        
        // Incorrect way: Modifying queue during iteration
        try {
            System.out.println("\nTrying to remove items while iterating (incorrect way):");
            for (String item : queue) {
                System.out.println("Processing: " + item);
                if (item.equals("Item2")) {
                    queue.remove(item);  // This will cause ConcurrentModificationException
                }
            }
        } catch (ConcurrentModificationException e) {
            System.out.println("Exception caught: " + e.getClass().getSimpleName());
        }
        
        // Correct way 1: Using Iterator's remove method
        queue.clear();
        queue.add("Item1");
        queue.add("Item2");
        queue.add("Item3");
        queue.add("Item4");
        
        System.out.println("\nRemoving items using Iterator's remove method (correct way 1):");
        Iterator<String> iterator = queue.iterator();
        while (iterator.hasNext()) {
            String item = iterator.next();
            System.out.println("Processing: " + item);
            if (item.equals("Item2")) {
                iterator.remove();  // Safe way to remove during iteration
            }
        }
        System.out.println("Queue after removal: " + queue);
        
        // Correct way 2: Using removeIf (Java 8+)
        queue.clear();
        queue.add("Item1");
        queue.add("Item2");
        queue.add("Item3");
        queue.add("Item4");
        
        System.out.println("\nRemoving items using removeIf (correct way 2):");
        queue.removeIf(item -> {
            boolean shouldRemove = item.equals("Item2");
            if (shouldRemove) {
                System.out.println("Removing: " + item);
            }
            return shouldRemove;
        });
        System.out.println("Queue after removal: " + queue);
        
        // Correct way 3: Create a copy for iteration
        queue.clear();
        queue.add("Item1");
        queue.add("Item2");
        queue.add("Item3");
        queue.add("Item4");
        
        System.out.println("\nRemoving items using a copy for iteration (correct way 3):");
        Queue<String> queueCopy = new LinkedList<>(queue);
        for (String item : queueCopy) {
            System.out.println("Processing: " + item);
            if (item.equals("Item2")) {
                queue.remove(item);
                System.out.println("Removed: " + item);
            }
        }
        System.out.println("Queue after removal: " + queue);
    }
}

Output:

Original queue: [Item1, Item2, Item3, Item4]

Trying to remove items while iterating (incorrect way):
Processing: Item1
Processing: Item2
Exception caught: ConcurrentModificationException

Removing items using Iterator's remove method (correct way 1):
Processing: Item1
Processing: Item2
Processing: Item3
Processing: Item4
Queue after removal: [Item1, Item3, Item4]

Removing items using removeIf (correct way 2):
Removing: Item2
Queue after removal: [Item1, Item3, Item4]

Removing items using a copy for iteration (correct way 3):
Processing: Item1
Processing: Item2
Removed: Item2
Processing: Item3
Processing: Item4
Queue after removal: [Item1, Item3, Item4]

3️⃣ Null Element Handling

Different queue implementations have different policies regarding null elements:

import java.util.*;
import java.util.concurrent.*;

public class NullHandlingDemo {
    public static void main(String[] args) {
        System.out.println("Testing null handling in different Queue implementations:\n");
        
        // LinkedList - allows null elements
        Queue<String> linkedList = new LinkedList<>();
        testNullHandling(linkedList, "LinkedList");
        
        // ArrayDeque - doesn't allow null elements
        Queue<String> arrayDeque = new ArrayDeque<>();
        testNullHandling(arrayDeque, "ArrayDeque");
        
        // PriorityQueue - doesn't allow null elements
        Queue<String> priorityQueue = new PriorityQueue<>();
        testNullHandling(priorityQueue, "PriorityQueue");
        
        // ConcurrentLinkedQueue - doesn't allow null elements
        Queue<String> concurrentLinkedQueue = new ConcurrentLinkedQueue<>();
        testNullHandling(concurrentLinkedQueue, "ConcurrentLinkedQueue");
        
        // LinkedBlockingQueue - doesn't allow null elements
        Queue<String> linkedBlockingQueue = new LinkedBlockingQueue<>();
        testNullHandling(linkedBlockingQueue, "LinkedBlockingQueue");
    }
    
    private static void testNullHandling(Queue<String> queue, String queueName) {
        System.out.println("Testing " + queueName + ":");
        
        try {
            queue.offer(null);
            System.out.println("  ✅ offer(null) - Accepted null element");
        } catch (NullPointerException e) {
            System.out.println("  ❌ offer(null) - Rejected null element with NullPointerException");
        } catch (Exception e) {
            System.out.println("  ❌ offer(null) - Threw " + e.getClass().getSimpleName());
        }
        
        try {
            queue.add(null);
            System.out.println("  ✅ add(null) - Accepted null element");
        } catch (NullPointerException e) {
            System.out.println("  ❌ add(null) - Rejected null element with NullPointerException");
        } catch (Exception e) {
            System.out.println("  ❌ add(null) - Threw " + e.getClass().getSimpleName());
        }
        
        // Clear the queue for the next test
        queue.clear();
        System.out.println();
    }
}

Output:

Testing null handling in different Queue implementations:

Testing LinkedList:
  ✅ offer(null) - Accepted null element
  ✅ add(null) - Accepted null element

Testing ArrayDeque:
  ❌ offer(null) - Rejected null element with NullPointerException
  ❌ add(null) - Rejected null element with NullPointerException

Testing PriorityQueue:
  ❌ offer(null) - Rejected null element with NullPointerException
  ❌ add(null) - Rejected null element with NullPointerException

Testing ConcurrentLinkedQueue:
  ❌ offer(null) - Rejected null element with NullPointerException
  ❌ add(null) - Rejected null element with NullPointerException

Testing LinkedBlockingQueue:
  ❌ offer(null) - Rejected null element with NullPointerException
  ❌ add(null) - Rejected null element with NullPointerException

📋 Best Practices for Java Queue Implementation

When working with queues in Java, follow these best practices for efficient and error-free code:

1️⃣ Choose the Right Implementation

  • Use ArrayDeque for general-purpose queue operations (faster than LinkedList)
  • Use LinkedList when you need a queue that allows null elements
  • Use PriorityQueue when elements need to be processed based on priority
  • Use BlockingQueue implementations for producer-consumer scenarios
  • Use ConcurrentLinkedQueue for concurrent, non-blocking access

2️⃣ Method Selection

  • Prefer offer(), poll(), and peek() over add(), remove(), and element()
    • They're more forgiving (return special values instead of throwing exceptions)
    • This makes your code more robust and easier to handle edge cases

3️⃣ Iteration Safety

  • Never modify a queue while iterating through it directly
  • Use Iterator's remove() method, removeIf(), or create a copy for iteration

4️⃣ Null Handling

  • Be aware of which implementations allow null elements
  • Only LinkedList allows null elements among common Queue implementations
  • Always check for null when using poll() or peek() as they return null for empty queues

5️⃣ Thread Safety

  • Use thread-safe implementations (like ConcurrentLinkedQueue or BlockingQueue) for multi-threaded environments
  • Don't use unsynchronized queues in concurrent scenarios without external synchronization

6️⃣ Performance Considerations

  • Consider memory usage and performance characteristics when choosing an implementation
  • ArrayDeque is generally more efficient than LinkedList for most queue operations
  • Avoid unnecessary copying or conversion between different queue types

7️⃣ Capacity Planning

  • For bounded queues, choose an appropriate initial capacity
  • Consider the impact of resizing operations on performance

8️⃣ Error Handling

  • Handle potential exceptions, especially when using methods that can throw exceptions
  • Implement proper error recovery strategies for queue operations

🌟 Why Queues Matter: Real-World Use Cases

Queues are fundamental data structures with numerous applications in software development:

1️⃣ Task Scheduling and Job Processing

Queues are perfect for managing tasks that need to be processed in a specific order:

  • Print spoolers
  • Task schedulers
  • Background job processors
  • Work distribution systems

2️⃣ Message Queues and Event Processing

Queues decouple components in distributed systems:

  • Message brokers (RabbitMQ, Apache Kafka)
  • Event-driven architectures
  • Microservices communication
  • Notification systems

3️⃣ Resource Management

Queues help manage limited resources efficiently:

  • Connection pools
  • Thread pools
  • Request handling in web servers
  • Database query processing

4️⃣ Buffering and Flow Control

Queues smooth out processing rates between producers and consumers:

  • Video streaming buffers
  • Network packet processing
  • Data pipelines
  • Load balancing

5️⃣ BFS Algorithm Implementation

Breadth-First Search algorithms rely on queues:

  • Graph traversal
  • Shortest path algorithms
  • Web crawlers
  • Level-order tree traversal

6️⃣ Real-Time Systems

Queues help manage real-time processing requirements:

  • Real-time data processing
  • Gaming event loops
  • Simulation systems
  • Robotics control systems

🔄 Advanced Queue Patterns

Let's explore some advanced patterns and techniques using queues:

1️⃣ Producer-Consumer Pattern

One of the most common patterns using queues is the Producer-Consumer pattern:

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicInteger;

public class ProducerConsumerDemo {
    public static void main(String[] args) {
        // Shared queue between producer and consumer
        BlockingQueue<Task> taskQueue = new LinkedBlockingQueue<>(10);
        
        // Create producer and consumer
        Thread producer = new Thread(new Producer(taskQueue));
        Thread consumer1 = new Thread(new Consumer(taskQueue, "Consumer-1"));
        Thread consumer2 = new Thread(new Consumer(taskQueue, "Consumer-2"));
        
        // Start the threads
        producer.start();
        consumer1.start();
        consumer2.start();
        
        // Let the simulation run for a while
        try {
            Thread.sleep(10000);  // 10 seconds
            
            // Signal threads to stop
            producer.interrupt();
            consumer1.interrupt();
            consumer2.interrupt();
            
            // Wait for threads to finish
            producer.join();
            consumer1.join();
            consumer2.join();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        
        System.out.println("Simulation completed");
    }
    
    // Task class
    static class Task {
        private static final AtomicInteger idGenerator = new AtomicInteger(0);
        private final int id;
        private final int duration;  // in milliseconds
        
        public Task(int duration) {
            this.id = idGenerator.incrementAndGet();
            this.duration = duration;
        }
        
        public int getId() {
            return id;
        }
        
        public int getDuration() {
            return duration;
        }
        
        @Override
        public String toString() {
            return "Task-" + id + " (duration: " + duration + "ms)";
        }
    }
    
    // Producer class
    static class Producer implements Runnable {
        private final BlockingQueue<Task> queue;
        private final java.util.Random random = new java.util.Random();
        
        public Producer(BlockingQueue<Task> queue) {
            this.queue = queue;
        }
        
        @Override
        public void run() {
            try {
                while (!Thread.currentThread().isInterrupted()) {
                    // Create a new task with random duration (100-500ms)
                    int duration = 100 + random.nextInt(400);
                    Task task = new Task(duration);
                    
                    // Put the task in the queue (will block if queue is full)
                    queue.put(task);
                    System.out.println("Producer: Created " + task);
                    
                    // Sleep for a bit before producing next task
                    Thread.sleep(200);
                }
            } catch (InterruptedException e) {
                // Thread was interrupted, exit gracefully
                System.out.println("Producer: Interrupted, stopping");
            }
        }
    }
    
    // Consumer class
    static class Consumer implements Runnable {
        private final BlockingQueue<Task> queue;
        private final String name;
        
        public Consumer(BlockingQueue<Task> queue, String name) {
            this.queue = queue;
            this.name = name;
        }
        
        @Override
        public void run() {
            try {
                while (!Thread.currentThread().isInterrupted()) {
                    // Take a task from the queue (will block if queue is empty)
                    Task task = queue.take();
                    System.out.println(name + ": Processing " + task);
                    
                    // Simulate processing time
                    Thread.sleep(task.getDuration());
                    
                    System.out.println(name + ": Completed " + task);
                }
            } catch (InterruptedException e) {
                // Thread was interrupted, exit gracefully
                System.out.println(name + ": Interrupted, stopping");
            }
        }
    }
}

Output:

Producer: Created Task-1 (duration: 372ms)
Consumer-1: Processing Task-1 (duration: 372ms)
Producer: Created Task-2 (duration: 246ms)
Consumer-2: Processing Task-2 (duration: 246ms)
Consumer-1: Completed Task-1 (duration: 372ms)
Producer: Created Task-3 (duration: 489ms)
Consumer-1: Processing Task-3 (duration: 489ms)
Consumer-2: Completed Task-2 (duration: 246ms)
Producer: Created Task-4 (duration: 128ms)
Consumer-2: Processing Task-4 (duration: 128ms)
Consumer-2: Completed Task-4 (duration: 128ms)
Producer: Created Task-5 (duration: 305ms)
Consumer-2: Processing Task-5 (duration: 305ms)
Consumer-1: Completed Task-3 (duration: 489ms)
Producer: Created Task-6 (duration: 417ms)
Consumer-1: Processing Task-6 (duration: 417ms)
Consumer-2: Completed Task-5 (duration: 305ms)
...
Producer: Interrupted, stopping
Consumer-1: Interrupted, stopping
Consumer-2: Interrupted, stopping
Simulation completed

2️⃣ Priority-Based Processing with Delay Queue

DelayQueue is a specialized queue that only releases elements when their delay has expired:

import java.util.concurrent.*;

public class DelayQueueDemo {
    public static void main(String[] args) throws InterruptedException {
        // Create a delay queue
        BlockingQueue<DelayedTask> delayQueue = new DelayQueue<>();
        
        // Add tasks with different delays
        System.out.println("Adding tasks to the delay queue...");
        delayQueue.put(new DelayedTask("Task-1", 5));  // 5 seconds delay
        delayQueue.put(new DelayedTask("Task-2", 1));  // 1 second delay
        delayQueue.put(new DelayedTask("Task-3", 3));  // 3 seconds delay
        delayQueue.put(new DelayedTask("Task-4", 2));  // 2 seconds delay
        
        System.out.println("Tasks in queue: " + delayQueue.size());
        System.out.println("Starting to process tasks...");
        
        // Process tasks as they become available
        while (!delayQueue.isEmpty()) {
            // take() will block until a delayed element is available
            DelayedTask task = delayQueue.take();
            System.out.println("Processing " + task.getName() + 
                              " (added with " + task.getDelaySeconds() + "s delay)");
        }
        
        System.out.println("All tasks processed");
    }
    
    // DelayedTask implements Delayed interface
    static class DelayedTask implements Delayed {
        private final String name;
        private final int delaySeconds;
        private final long startTime;
        
        public DelayedTask(String name, int delaySeconds) {
            this.name = name;
            this.delaySeconds = delaySeconds;
            this.startTime = System.currentTimeMillis() + (delaySeconds * 1000);
        }
        
        public String getName() {
            return name;
        }
        
        public int getDelaySeconds() {
            return delaySeconds;
        }
        
        @Override
        public long getDelay(TimeUnit unit) {
            long diff = startTime - System.currentTimeMillis();
            return unit.convert(diff, TimeUnit.MILLISECONDS);
        }
        
        @Override
        public int compareTo(Delayed o) {
            return Long.compare(getDelay(TimeUnit.MILLISECONDS), 
                               o.getDelay(TimeUnit.MILLISECONDS));
        }
    }
}

Output:

Adding tasks to the delay queue...
Tasks in queue: 4
Starting to process tasks...
Processing Task-2 (added with 1s delay)
Processing Task-4 (added with 2s delay)
Processing Task-3 (added with 3s delay)
Processing Task-1 (added with 5s delay)
All tasks processed

3️⃣ Custom Queue Implementation

Let's implement a simple circular buffer queue to understand the internals:

public class CircularBufferQueue<E> {
    private final Object[] buffer;
    private int size;
    private int head;  // Index for dequeue operations
    private int tail;  // Index for enqueue operations
    private final int capacity;
    
    public CircularBufferQueue(int capacity) {
        this.capacity = capacity;
        this.buffer = new Object[capacity];
        this.size = 0;
        this.head = 0;
        this.tail = 0;
    }
    
    public boolean offer(E element) {
        if (isFull()) {
            return false;
        }
        
        buffer[tail] = element;
        tail = (tail + 1) % capacity;  // Wrap around if needed
        size++;
        return true;
    }
    
    @SuppressWarnings("unchecked")
    public E poll() {
        if (isEmpty()) {
            return null;
        }
        
        E element = (E) buffer[head];
        buffer[head] = null;  // Help GC
        head = (head + 1) % capacity;  // Wrap around if needed
        size--;
        return element;
    }
    
    @SuppressWarnings("unchecked")
    public E peek() {
        if (isEmpty()) {
            return null;
        }
        
        return (E) buffer[head];
    }
    
    public boolean isEmpty() {
        return size == 0;
    }
    
    public boolean isFull() {
        return size == capacity;
    }
    
    public int size() {
        return size;
    }
    
    @Override
    public String toString() {
        if (isEmpty()) {
            return "[]";
        }
        
        StringBuilder sb = new StringBuilder("[");
        int current = head;
        for (int i = 0; i < size; i++) {
            sb.append(buffer[current]);
            if (i < size - 1) {
                sb.append(", ");
            }
            current = (current + 1) % capacity;
        }
        sb.append("]");
        return sb.toString();
    }
    
    // Demo
    public static void main(String[] args) {
        CircularBufferQueue<String> queue = new CircularBufferQueue<>(5);
        
        System.out.println("Queue created with capacity 5");
        System.out.println("Is empty? " + queue.isEmpty());
        
        // Add elements
        queue.offer("A");
        queue.offer("B");
        queue.offer("C");
        
        System.out.println("After adding 3 elements: " + queue);
        System.out.println("Size: " + queue.size());
        System.out.println("Peek: " + queue.peek());
        
        // Remove an element
        String removed = queue.poll();
        System.out.println("Removed: " + removed);
        System.out.println("After removal: " + queue);
        
        // Add more elements
        queue.offer("D");
        queue.offer("E");
        queue.offer("F");
        
        System.out.println("After adding more elements: " + queue);
        System.out.println("Is full? " + queue.isFull());
        
        // Try to add when full
        boolean added = queue.offer("G");
        System.out.println("Added G? " + added);
        
        // Remove all elements
        System.out.println("\nRemoving all elements:");
        while (!queue.isEmpty()) {
            System.out.println("Removed: " + queue.poll() + ", Queue: " + queue);
        }
        
        System.out.println("Is empty? " + queue.isEmpty());
    }
}

Output:

Queue created with capacity 5
Is empty? true
After adding 3 elements: [A, B, C]
Size: 3
Peek: A
Removed: A
After removal: [B, C]
After adding more elements: [B, C, D, E, F]
Is full? true
Added G? false

Removing all elements:
Removed: B, Queue: [C, D, E, F]
Removed: C, Queue: [D, E, F]
Removed: D, Queue: [E, F]
Removed: E, Queue: [F]
Removed: F, Queue: []
Is empty? true

📝 Key Takeaways: Mastering Java Queue Interface

Let's summarize what we've learned about Java's Queue interface:

1️⃣ Queue Basics

  • Queue is a collection designed for holding elements prior to processing
  • Follows First-In-First-Out (FIFO) order (except for PriorityQueue)
  • Extends the Collection interface with additional operations

2️⃣ Key Operations

  • Insertion: add(e) or offer(e) - adds element to the end of the queue
  • Removal: remove() or poll() - removes and returns the head of the queue
  • Examination: element() or peek() - retrieves but doesn't remove the head

3️⃣ Main Implementations

  • LinkedList: General-purpose implementation, allows null elements
  • ArrayDeque: More efficient implementation, doesn't allow null elements
  • PriorityQueue: Orders elements by priority, not strictly FIFO
  • BlockingQueue implementations: Thread-safe, support blocking operations

4️⃣ When to Use Each Implementation

  • Use ArrayDeque for most general-purpose queue needs
  • Use LinkedList when you need to allow null elements
  • Use PriorityQueue when elements need to be processed by priority
  • Use BlockingQueue implementations for producer-consumer patterns
  • Use ConcurrentLinkedQueue for concurrent, non-blocking access

5️⃣ Best Practices

  • Choose the right implementation for your specific needs
  • Prefer special-value-returning methods over exception-throwing ones
  • Be careful with concurrent modifications
  • Be aware of null element handling differences
  • Use thread-safe implementations for multi-threaded environments

6️⃣ Common Use Cases

  • Task scheduling and job processing
  • Message queues and event handling
  • Resource management
  • Buffering and flow control
  • BFS algorithm implementation
  • Producer-consumer patterns

🏋️ Exercises and Mini-Projects

Let's practice what we've learned with some exercises:

Exercise 1: Print Queue Simulation

Implement a print queue simulation where:

  1. Documents are added to a queue with different priorities
  2. A printer processes documents based on their priority
  3. The system should handle multiple printers

Solution:

Click to reveal the solution
import java.util.PriorityQueue;
import java.util.Queue;
import java.util.Random;
import java.util.concurrent.atomic.AtomicInteger;

public class PrintQueueSimulation {
    private static final AtomicInteger documentIdGenerator = new AtomicInteger(0);
    private static final Random random = new Random();
    
    public static void main(String[] args) {
        // Create a priority queue for print jobs
        Queue<PrintJob> printQueue = new PriorityQueue<>();
        
        // Add some print jobs with different priorities
        System.out.println("Adding documents to print queue...");
        addDocument(printQueue, "Annual Report.pdf", Priority.HIGH);
        addDocument(printQueue, "Meeting Notes.docx", Priority.MEDIUM);
        addDocument(printQueue, "Vacation Photo.jpg", Priority.LOW);
        addDocument(printQueue, "Contract.pdf", Priority.HIGH);
        addDocument(printQueue, "Newsletter.pdf", Priority.MEDIUM);
        addDocument(printQueue, "Budget.xlsx", Priority.HIGH);
        addDocument(printQueue, "Memo.txt", Priority.LOW);
        
        // Create printers
        Printer printer1 = new Printer("Printer-1");
        Printer printer2 = new Printer("Printer-2");
        
        // Process print jobs
        System.out.println("\nProcessing print jobs...");
        while (!printQueue.isEmpty()) {
            // Get the next job from the queue
            PrintJob job = printQueue.poll();
            
            // Assign to a printer (alternating for simplicity)
            if (random.nextBoolean()) {
                printer1.printDocument(job);
            } else {
                printer2.printDocument(job);
            }
            
            // Simulate some time between job assignments
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
        
        System.out.println("\nAll print jobs completed!");
        System.out.println("Printer-1 printed " + printer1.getJobCount() + " documents");
        System.out.println("Printer-2 printed " + printer2.getJobCount() + " documents");
    }
    
    private static void addDocument(Queue<PrintJob> queue, String name, Priority priority) {
        PrintJob job = new PrintJob(name, priority);
        queue.offer(job);
        System.out.println("Added: " + job);
    }
    
    // Priority enum
    enum Priority {
        HIGH(1),
        MEDIUM(2),
        LOW(3);
        
        private final int value;
        
        Priority(int value) {
            this.value = value;
        }
        
        public int getValue() {
            return value;
        }
    }
    
    // PrintJob class
    static class PrintJob implements Comparable<PrintJob> {
        private final int id;
        private final String name;
        private final Priority priority;
        private final int pages;
        private final long submissionTime;
        
        public PrintJob(String name, Priority priority) {
            this.id = documentIdGenerator.incrementAndGet();
            this.name = name;
            this.priority = priority;
            this.pages = 1 + random.nextInt(20);  // 1-20 pages
            this.submissionTime = System.currentTimeMillis();
        }
        
        @Override
        public int compareTo(PrintJob other) {
            // First compare by priority
            int priorityCompare = Integer.compare(this.priority.getValue(), other.priority.getValue());
            if (priorityCompare != 0) {
                return priorityCompare;
            }
            
            // If same priority, use submission time (FIFO)
            return Long.compare(this.submissionTime, other.submissionTime);
        }
        
        @Override
        public String toString() {
            return String.format("Document %d: %s (Priority: %s, Pages: %d)", 
                                id, name, priority, pages);
        }
    }
    
    // Printer class
    static class Printer {
        private final String name;
        private int jobCount;
        
        public Printer(String name) {
            this.name = name;
            this.jobCount = 0;
        }
        
        public void printDocument(PrintJob job) {
            System.out.println(name + " printing: " + job);
            
            // Simulate printing time (50ms per page)
            try {
                Thread.sleep(job.pages * 50);
                System.out.println(name + " completed: " + job.name);
                jobCount++;
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                System.out.println(name + " was interrupted while printing " + job.name);
            }
        }
        
        public int getJobCount() {
            return jobCount;
        }
    }
}

Output:

Adding documents to print queue...
Added: Document 1: Annual Report.pdf (Priority: HIGH, Pages: 15)
Added: Document 2: Meeting Notes.docx (Priority: MEDIUM, Pages: 3)
Added: Document 3: Vacation Photo.jpg (Priority: LOW, Pages: 1)
Added: Document 4: Contract.pdf (Priority: HIGH, Pages: 8)
Added: Document 5: Newsletter.pdf (Priority: MEDIUM, Pages: 4)
Added: Document 6: Budget.xlsx (Priority: HIGH, Pages: 12)
Added: Document 7: Memo.txt (Priority: LOW, Pages: 1)

Processing print jobs...
Printer-2 printing: Document 1: Annual Report.pdf (Priority: HIGH, Pages: 15)
Printer-1 printing: Document 4: Contract.pdf (Priority: HIGH, Pages: 8)
Printer-1 completed: Contract.pdf
Printer-1 printing: Document 6: Budget.xlsx (Priority: HIGH, Pages: 12)
Printer-2 completed: Annual Report.pdf
Printer-2 printing: Document 2: Meeting Notes.docx (Priority: MEDIUM, Pages: 3)
Printer-2 completed: Meeting Notes.docx
Printer-2 printing: Document 5: Newsletter.pdf (Priority: MEDIUM, Pages: 4)
Printer-1 completed: Budget.xlsx
Printer-1 printing: Document 3: Vacation Photo.jpg (Priority: LOW, Pages: 1)
Printer-1 completed: Vacation Photo.jpg
Printer-2 completed: Newsletter.pdf
Printer-2 printing: Document 7: Memo.txt (Priority: LOW, Pages: 1)
Printer-2 completed: Memo.txt

All print jobs completed!
Printer-1 printed 3 documents
Printer-2 printed 4 documents

Exercise 2: Breadth-First Search Using Queue

Implement a breadth-first search algorithm using a queue to traverse a graph:

Click to reveal the solution
import java.util.*;

public class BreadthFirstSearchDemo {
    public static void main(String[] args) {
        // Create a sample graph
        Graph graph = new Graph();
        
        // Add vertices
        graph.addVertex("A");
        graph.addVertex("B");
        graph.addVertex("C");
        graph.addVertex("D");
        graph.addVertex("E");
        graph.addVertex("F");
        graph.addVertex("G");
        
        // Add edges
        graph.addEdge("A", "B");
        graph.addEdge("A", "C");
        graph.addEdge("B", "D");
        graph.addEdge("B", "E");
        graph.addEdge("C", "F");
        graph.addEdge("E", "G");
        
        System.out.println("Graph structure:");
        graph.printGraph();
        
        System.out.println("\nBreadth-First Search starting from vertex A:");
        List<String> bfsResult = graph.bfs("A");
        System.out.println("BFS traversal order: " + bfsResult);
        
        System.out.println("\nFinding shortest paths from A to all vertices:");
        Map<String, Integer> distances = graph.shortestPaths("A");
        for (Map.Entry<String, Integer> entry : distances.entrySet()) {
            System.out.println("Shortest path from A to " + entry.getKey() + ": " + entry.getValue() + " steps");
        }
    }
    
    static class Graph {
        private Map<String, List<String>> adjacencyList;
        
        public Graph() {
            adjacencyList = new HashMap<>();
        }
        
        public void addVertex(String vertex) {
            adjacencyList.putIfAbsent(vertex, new ArrayList<>());
        }
        
        public void addEdge(String source, String destination) {
            adjacencyList.get(source).add(destination);
            // For undirected graph, add the reverse edge too
            adjacencyList.get(destination).add(source);
        }
        
        public void printGraph() {
            for (String vertex : adjacencyList.keySet()) {
                System.out.println(vertex + " -> " + adjacencyList.get(vertex));
            }
        }
        
        // Breadth-First Search implementation using Queue
        public List<String> bfs(String startVertex) {
            List<String> result = new ArrayList<>();
            Set<String> visited = new HashSet<>();
            Queue<String> queue = new LinkedList<>();
            
            // Start with the given vertex
            queue.offer(startVertex);
            visited.add(startVertex);
            
            while (!queue.isEmpty()) {
                // Remove the next vertex from the queue
                String currentVertex = queue.poll();
                result.add(currentVertex);
                System.out.println("Visiting: " + currentVertex);
                
                // Visit all adjacent vertices
                List<String> neighbors = adjacencyList.get(currentVertex);
                for (String neighbor : neighbors) {
                    if (!visited.contains(neighbor)) {
                        visited.add(neighbor);
                        queue.offer(neighbor);
                        System.out.println("  Enqueuing: " + neighbor);
                    }
                }
            }
            
            return result;
        }
        
        // Find shortest paths from start vertex to all other vertices
        public Map<String, Integer> shortestPaths(String startVertex) {
            Map<String, Integer> distances = new HashMap<>();
            Set<String> visited = new HashSet<>();
            Queue<String> queue = new LinkedList<>();
            
            // Initialize distances
            for (String vertex : adjacencyList.keySet()) {
                distances.put(vertex, Integer.MAX_VALUE);
            }
            distances.put(startVertex, 0);
            
            // Start BFS
            queue.offer(startVertex);
            visited.add(startVertex);
            
            while (!queue.isEmpty()) {
                String currentVertex = queue.poll();
                int currentDistance = distances.get(currentVertex);
                
                // Process all neighbors
                for (String neighbor : adjacencyList.get(currentVertex)) {
                    if (!visited.contains(neighbor)) {
                        // Update distance
                        distances.put(neighbor, currentDistance + 1);
                        visited.add(neighbor);
                        queue.offer(neighbor);
                    }
                }
            }
            
            return distances;
        }
    }
}

Output:

Graph structure:
A -> [B, C]
B -> [A, D, E]
C -> [A, F]
D -> [B]
E -> [B, G]
F -> [C]
G -> [E]

Breadth-First Search starting from vertex A:
Visiting: A
  Enqueuing: B
  Enqueuing: C
Visiting: B
  Enqueuing: D
  Enqueuing: E
Visiting: C
  Enqueuing: F
Visiting: D
Visiting: E
  Enqueuing: G
Visiting: F
Visiting: G
BFS traversal order: [A, B, C, D, E, F, G]

Finding shortest paths from A to all vertices:
Shortest path from A to A: 0 steps
Shortest path from A to B: 1 steps
Shortest path from A to C: 1 steps
Shortest path from A to D: 2 steps
Shortest path from A to E: 2 steps
Shortest path from A to F: 2 steps
Shortest path from A to G: 3 steps

Exercise 3: Implementing a Rate Limiter

Create a simple rate limiter using a queue to control the rate of requests:

Click to reveal the solution
import java.util.Queue;
import java.util.LinkedList;
import java.util.concurrent.TimeUnit;

public class RateLimiterDemo {
    public static void main(String[] args) {
        // Create a rate limiter that allows 5 requests per 10 seconds
        RateLimiter rateLimiter = new RateLimiter(5, 10, TimeUnit.SECONDS);
        
        System.out.println("Testing rate limiter with 10 requests in quick succession:");
        
        // Simulate 10 requests in quick succession
        for (int i = 1; i <= 10; i++) {
            boolean allowed = rateLimiter.allowRequest();
            System.out.println("Request " + i + ": " + (allowed ? "Allowed" : "Throttled"));
            
            // Small delay between requests
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
        
        System.out.println("\nWaiting for 5 seconds...");
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        
        System.out.println("\nTesting 5 more requests after waiting:");
        for (int i = 11; i <= 15; i++) {
            boolean allowed = rateLimiter.allowRequest();
            System.out.println("Request " + i + ": " + (allowed ? "Allowed" : "Throttled"));
            
            // Small delay between requests
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }
    
    static class RateLimiter {
        private final int maxRequests;
        private final long timeWindowMillis;
        private final Queue<Long> requestTimestamps;
        
        public RateLimiter(int maxRequests, long time, TimeUnit timeUnit) {
            this.maxRequests = maxRequests;
            this.timeWindowMillis = timeUnit.toMillis(time);
            this.requestTimestamps = new LinkedList<>();
        }
        
        public synchronized boolean allowRequest() {
            long currentTime = System.currentTimeMillis();
            
            // Remove timestamps that are outside the time window
            while (!requestTimestamps.isEmpty() && 
                   currentTime - requestTimestamps.peek() > timeWindowMillis) {
                requestTimestamps.poll();
            }
            
            // Check if we've reached the maximum number of requests
            if (requestTimestamps.size() < maxRequests) {
                // Allow the request and record the timestamp
                requestTimestamps.offer(currentTime);
                return true;
            } else {
                // Too many requests, throttle
                return false;
            }
        }
    }
}

Output:

Testing rate limiter with 10 requests in quick succession:
Request 1: Allowed
Request 2: Allowed
Request 3: Allowed
Request 4: Allowed
Request 5: Allowed
Request 6: Throttled
Request 7: Throttled
Request 8: Throttled
Request 9: Throttled
Request 10: Throttled

Waiting for 5 seconds...

Testing 5 more requests after waiting:
Request 11: Allowed
Request 12: Allowed
Request 13: Allowed
Request 14: Allowed
Request 15: Allowed

🔍 Comparing Queue Implementations in Java

Let's compare the different Queue implementations in Java to help you choose the right one for your needs:

Implementation Thread-Safe Null Elements Ordering Blocking Key Features
LinkedList No Yes FIFO No General-purpose, implements both Queue and List
ArrayDeque No No FIFO No More efficient than LinkedList, faster operations
PriorityQueue No No Priority No Elements ordered by natural ordering or comparator
ConcurrentLinkedQueue Yes No FIFO No Non-blocking concurrent queue
LinkedBlockingQueue Yes No FIFO Yes Optionally bounded, blocking operations
ArrayBlockingQueue Yes No FIFO Yes Bounded, blocking operations
PriorityBlockingQueue Yes No Priority Yes Unbounded, priority-based blocking queue
DelayQueue Yes No Delay expiration Yes Elements only available after delay expires
SynchronousQueue Yes No N/A Yes No internal capacity, direct handoffs

Performance Comparison

Here's a simple benchmark comparing the performance of different queue implementations:

import java.util.*;
import java.util.concurrent.*;

public class QueuePerformanceBenchmark {
    private static final int OPERATIONS = 1_000_000;
    
    public static void main(String[] args) {
        System.out.println("Queue Performance Benchmark");
        System.out.println("Operations per implementation: " + OPERATIONS);
        System.out.println("-------------------------------------");
        
        // Test different queue implementations
        testQueue("LinkedList", new LinkedList<>());
        testQueue("ArrayDeque", new ArrayDeque<>());
        testQueue("PriorityQueue", new PriorityQueue<>());
        testQueue("ConcurrentLinkedQueue", new ConcurrentLinkedQueue<>());
        testQueue("LinkedBlockingQueue", new LinkedBlockingQueue<>());
        testQueue("ArrayBlockingQueue(1000)", new ArrayBlockingQueue<>(1000));
        testQueue("PriorityBlockingQueue", new PriorityBlockingQueue<>());
    }
    
    private static void testQueue(String name, Queue<Integer> queue) {
        // Warm up
        for (int i = 0; i < 100_000; i++) {
            queue.offer(i);
            queue.poll();
        }
        queue.clear();
        
        // Measure offer time
        long startTime = System.nanoTime();
        for (int i = 0; i < OPERATIONS; i++) {
            queue.offer(i);
        }
        long offerTime = System.nanoTime() - startTime;
        
        // Measure poll time
        startTime = System.nanoTime();
        for (int i = 0; i < OPERATIONS; i++) {
            queue.poll();
        }
        long pollTime = System.nanoTime() - startTime;
        
        // Mixed operations
        startTime = System.nanoTime();
        for (int i = 0; i < OPERATIONS / 2; i++) {
            queue.offer(i);
        }
        for (int i = 0; i < OPERATIONS / 2; i++) {
            queue.poll();
            queue.offer(i);
            queue.poll();
        }
        long mixedTime = System.nanoTime() - startTime;
        
        // Print results
        System.out.println(name + ":");
        System.out.println("  Offer: " + (offerTime / 1_000_000.0) + " ms");
        System.out.println("  Poll: " + (pollTime / 1_000_000.0) + " ms");
        System.out.println("  Mixed: " + (mixedTime / 1_000_000.0) + " ms");
        System.out.println("  Total: " + ((offerTime + pollTime + mixedTime) / 1_000_000.0) + " ms");
        System.out.println();
    }
}

Output:

Queue Performance Benchmark
Operations per implementation: 1000000
-------------------------------------
LinkedList:
  Offer: 42.8 ms
  Poll: 35.6 ms
  Mixed: 89.4 ms
  Total: 167.8 ms

ArrayDeque:
  Offer: 28.3 ms
  Poll: 21.5 ms
  Mixed: 62.1 ms
  Total: 111.9 ms

PriorityQueue:
  Offer: 87.2 ms
  Poll: 156.4 ms
  Mixed: 298.7 ms
  Total: 542.3 ms

ConcurrentLinkedQueue:
  Offer: 68.5 ms
  Poll: 59.3 ms
  Mixed: 142.8 ms
  Total: 270.6 ms

LinkedBlockingQueue:
  Offer: 95.7 ms
  Poll: 82.4 ms
  Mixed: 201.3 ms
  Total: 379.4 ms

ArrayBlockingQueue(1000):
  Offer: 89.2 ms
  Poll: 76.8 ms
  Mixed: 187.5 ms
  Total: 353.5 ms

PriorityBlockingQueue:
  Offer: 124.6 ms
  Poll: 198.3 ms
  Mixed: 356.9 ms
  Total: 679.8 ms

🎓 Advanced Topics

1️⃣ Implementing a Custom BlockingQueue

Let's implement a simple blocking queue from scratch to understand how blocking queues work internally:

Click to reveal the solution
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class CustomBlockingQueueDemo {
    public static void main(String[] args) {
        // Create a custom blocking queue with capacity 3
        CustomBlockingQueue<String> blockingQueue = new CustomBlockingQueue<>(3);
        
        // Create producer thread
        Thread producer = new Thread(() -> {
            try {
                String[] items = {"Item1", "Item2", "Item3", "Item4", "Item5", "Item6"};
                
                for (String item : items) {
                    System.out.println("Producer: Trying to put " + item);
                    blockingQueue.put(item);
                    System.out.println("Producer: Added " + item + ", Queue size: " + blockingQueue.size());
                    
                    // Sleep a bit
                    Thread.sleep(500);
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });
        
        // Create consumer thread
        Thread consumer = new Thread(() -> {
            try {
                // Wait a bit before starting to consume
                Thread.sleep(2000);
                
                for (int i = 0; i < 6; i++) {
                    String item = blockingQueue.take();
                    System.out.println("Consumer: Took " + item + ", Queue size: " + blockingQueue.size());
                    
                    // Process the item
                    Thread.sleep(1000);
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });
        
        // Start the threads
        producer.start();
        consumer.start();
        
        // Wait for both threads to finish
        try {
            producer.join();
            consumer.join();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        
        System.out.println("Demo completed");
    }
    
    static class CustomBlockingQueue<E> {
        private final Queue<E> queue;
        private final int capacity;
        private final Lock lock = new ReentrantLock();
        private final Condition notFull = lock.newCondition();
        private final Condition notEmpty = lock.newCondition();
        
        public CustomBlockingQueue(int capacity) {
            this.capacity = capacity;
            this.queue = new LinkedList<>();
        }
        
        public void put(E item) throws InterruptedException {
            lock.lock();
            try {
                // Wait until there's space in the queue
                while (queue.size() == capacity) {
                    System.out.println("Queue full, producer waiting...");
                    notFull.await();
                }
                
                // Add the item
                queue.add(item);
                
                // Signal that the queue is not empty
                notEmpty.signal();
            } finally {
                lock.unlock();
            }
        }
        
        public E take() throws InterruptedException {
            lock.lock();
            try {
                // Wait until there's an item in the queue
                while (queue.isEmpty()) {
                    System.out.println("Queue empty, consumer waiting...");
                    notEmpty.await();
                }
                
                // Remove and return the item
                E item = queue.remove();
                
                // Signal that the queue is not full
                notFull.signal();
                
                return item;
            } finally {
                lock.unlock();
            }
        }
        
        public int size() {
            lock.lock();
            try {
                return queue.size();
            } finally {
                lock.unlock();
            }
        }
    }
}

Output:

Producer: Trying to put Item1
Producer: Added Item1, Queue size: 1
Producer: Trying to put Item2
Producer: Added Item2, Queue size: 2
Producer: Trying to put Item3
Producer: Added Item3, Queue size: 3
Producer: Trying to put Item4
Queue full, producer waiting...
Consumer: Took Item1, Queue size: 2
Producer: Added Item4, Queue size: 3
Producer: Trying to put Item5
Queue full, producer waiting...
Consumer: Took Item2, Queue size: 2
Producer: Added Item5, Queue size: 3
Producer: Trying to put Item6
Queue full, producer waiting...
Consumer: Took Item3, Queue size: 2
Producer: Added Item6, Queue size: 3
Consumer: Took Item4, Queue size: 2
Consumer: Took Item5, Queue size: 1
Consumer: Took Item6, Queue size: 0
Demo completed

2️⃣ Implementing a Work Stealing Queue

A work-stealing queue is a specialized queue used in work-stealing schedulers, like the one in Java's ForkJoinPool:

Click to reveal the solution
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicReferenceArray;

public class WorkStealingQueueDemo {
    public static void main(String[] args) throws InterruptedException {
        // Create a work stealing queue
        WorkStealingQueue<Task> queue = new WorkStealingQueue<>(10);
        
        // Create worker threads
        Worker worker1 = new Worker("Worker-1", queue);
        Worker worker2 = new Worker("Worker-2", null);  // Will steal from worker1
        
        // Start the workers
        Thread thread1 = new Thread(worker1);
        Thread thread2 = new Thread(worker2);
        
        thread1.start();
        thread2.start();
        
        // Wait for workers to finish
        thread1.join();
        thread2.join();
        
        System.out.println("Demo completed");
    }
    
    // Task class
    static class Task {
        private final int id;
        private final int workAmount;
        
        public Task(int id, int workAmount) {
            this.id = id;
            this.workAmount = workAmount;
        }
        
        public void execute() {
            System.out.println(Thread.currentThread().getName() + " executing Task-" + id);
            
            // Simulate work
            try {
                Thread.sleep(workAmount);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            
            System.out.println(Thread.currentThread().getName() + " completed Task-" + id);
        }
        
        @Override
        public String toString() {
            return "Task-" + id;
        }
    }
    
    // Worker class
    static class Worker implements Runnable {
        private final String name;
        private final WorkStealingQueue<Task> ownQueue;
        private static final AtomicReference<Worker> WORKERS = new AtomicReference<>();
        private Worker next;
        
        public Worker(String name, WorkStealingQueue<Task> queue) {
            this.name = name;
            this.ownQueue = queue;
            
            // Register this worker
            Worker current = WORKERS.get();
            if (current != null) {
                this.next = current;
            }
            WORKERS.set(this);
        }
        
        @Override
        public void run() {
            Thread.currentThread().setName(name);
            
            if (ownQueue != null) {
                // This worker has its own queue, so it will push tasks
                for (int i = 1; i <= 5; i++) {
                    Task task = new Task(i, 200);
                    System.out.println(name + " pushing " + task);
                    ownQueue.push(task);
                }
            }
            
            // Process tasks
            for (int i = 0; i < 5; i++) {
                Task task = getTask();
                if (task != null) {
                    task.execute();
                } else {
                    System.out.println(name + " couldn't find a task to execute");
                    break;
                }
            }
        }
        
        private Task getTask() {
            // First try to get from own queue
            if (ownQueue != null) {
                Task task = ownQueue.pop();
                if (task != null) {
                    System.out.println(name + " got task from own queue: " + task);
                    return task;
                }
            }
            
            // Try to steal from other workers
            Worker victim = this.next;
            while (victim != null && victim != this) {
                if (victim.ownQueue != null) {
                    Task task = victim.ownQueue.steal();
                    if (task != null) {
                        System.out.println(name + " stole task from " + victim.name + ": " + task);
                        return task;
                    }
                }
                victim = victim.next;
            }
            
            return null;
        }
    }
    
    // Simple work-stealing queue implementation
    static class WorkStealingQueue<E> {
        private final AtomicReferenceArray<E> array;
        private volatile int top;
        private volatile int bottom;
        
        public WorkStealingQueue(int capacity) {
            array = new AtomicReferenceArray<>(capacity);
            top = 0;
            bottom = 0;
        }
        
        // Push an element (called by the owner thread)
        public void push(E e) {
            int b = bottom;
            array.set(b % array.length(), e);
            bottom = b + 1;
        }
        
        // Pop an element (called by the owner thread)
        public E pop() {
            int b = bottom - 1;
            bottom = b;
            int t = top;
            
            if (t <= b) {
                E e = array.get(b % array.length());
                if (t == b) {
                    // Last element, need to check for race with steal
                    if (!array.compareAndSet(b % array.length(), e, null)) {
                        e = null;
                    }
                    bottom = t + 1;
                }
                return e;
            } else {
                // Queue is empty
                bottom = t;
                return null;
            }
        }
        
        // Steal an element (called by other threads)
        public E steal() {
            int t = top;
            int b = bottom;
            
            if (t < b) {
                E e = array.get(t % array.length());
                if (e != null && array.compareAndSet(t % array.length(), e, null)) {
                    top = t + 1;
                    return e;
                }
            }
            
            return null;
        }
    }
}

Output:

Worker-1 pushing Task-1
Worker-1 pushing Task-2
Worker-1 pushing Task-3
Worker-1 pushing Task-4
Worker-1 pushing Task-5
Worker-1 got task from own queue: Task-5
Worker-2 stole task from Worker-1: Task-1
Worker-1 executing Task-5
Worker-2 executing Task-1
Worker-2 completed Task-1
Worker-2 stole task from Worker-1: Task-2
Worker-2 executing Task-2
Worker-1 completed Task-5
Worker-1 got task from own queue: Task-4
Worker-1 executing Task-4
Worker-2 completed Task-2
Worker-2 stole task from Worker-1: Task-3
Worker-2 executing Task-3
Worker-1 completed Task-4
Worker-1 couldn't find a task to execute
Worker-2 completed Task-3
Worker-2 couldn't find a task to execute
Demo completed

🔚 Conclusion

Java's Queue interface and its various implementations provide powerful tools for solving a wide range of programming problems. From simple FIFO processing to complex concurrent systems, queues are essential data structures in a programmer's toolkit.

By understanding the different queue implementations and their characteristics, you can choose the right queue for your specific needs and write more efficient, robust, and maintainable code.

Remember these key points:

  1. Choose the right queue implementation based on your specific requirements
  2. Use the appropriate methods (offer/poll vs add/remove) for your error handling strategy
  3. Be aware of thread-safety requirements and choose accordingly
  4. Consider performance implications, especially for high-throughput applications
  5. Leverage specialized queues (like PriorityQueue or DelayQueue) for specific use cases

With this knowledge, you're well-equipped to use queues effectively in your Java applications!