Java LinkedBlockingQueue


In Java, managing data between multiple threads can be challenging, especially when synchronization is required. One of the key utilities provided by the java.util.concurrent package is the LinkedBlockingQueue class. It is an implementation of the BlockingQueue interface that uses a linked-node structure to store elements. Unlike ArrayBlockingQueue, which has a fixed size, LinkedBlockingQueue can be either bounded (with a fixed capacity) or unbounded, making it a versatile choice for many concurrent programming tasks.


What is LinkedBlockingQueue in Java?

The LinkedBlockingQueue is a thread-safe queue used for managing data in concurrent applications. It implements the BlockingQueue interface, which offers blocking operations like put() and take(), enabling threads to safely interact with the queue without the risk of race conditions.

The key characteristic of LinkedBlockingQueue is that it can be either bounded or unbounded:

  • Bounded: If a capacity is specified during construction, it can store a limited number of elements. Once full, the producer thread will be blocked until space becomes available.
  • Unbounded: If no capacity is specified, the queue can grow indefinitely, providing more flexibility for scenarios where a fixed size isn't necessary.

Key features:

  • Thread-Safety: All operations on LinkedBlockingQueue are thread-safe.
  • Blocking Operations: Offers blocking methods such as put() and take(), ideal for managing resources between threads.
  • Fairness Policy: Allows for the specification of a fairness policy when creating the queue, ensuring that threads acquire elements in a first-come-first-served manner.

Constructor of LinkedBlockingQueue

You can create a LinkedBlockingQueue with the following constructors:

LinkedBlockingQueue(int capacity)       // Creates a bounded queue with the specified capacity
LinkedBlockingQueue()                  // Creates an unbounded queue
LinkedBlockingQueue(int capacity, boolean fair)  // Creates a bounded queue with a fairness policy
  • capacity: The maximum number of elements the queue can hold (for bounded queues).
  • fair: If true, the queue will guarantee FIFO order, ensuring that threads acquire resources in the order they requested them.

Key Methods in LinkedBlockingQueue

LinkedBlockingQueue provides several key methods for inserting, removing, and inspecting elements in the queue:

  • put(E e): Adds an element to the queue, blocking until space becomes available if the queue is full.
  • take(): Retrieves and removes the head of the queue, blocking until an element is available.
  • offer(E e): Attempts to insert the specified element into the queue without blocking, returning false if the queue is full.
  • poll(): Retrieves and removes the head of the queue, returning null if the queue is empty.
  • offer(E e, long timeout, TimeUnit unit): Attempts to insert an element with a timeout, blocking if necessary for the specified time.
  • poll(long timeout, TimeUnit unit): Retrieves and removes the head of the queue, blocking for a specified time if necessary.
  • remainingCapacity(): Returns the number of remaining spaces in the queue.
  • clear(): Removes all elements from the queue.

Example 1: Basic Usage of LinkedBlockingQueue

Here is a simple example of how to use a LinkedBlockingQueue in a producer-consumer scenario.

import java.util.concurrent.LinkedBlockingQueue;

public class Main {
    public static void main(String[] args) throws InterruptedException {
        // Create a LinkedBlockingQueue with a capacity of 2
        LinkedBlockingQueue<String> queue = new LinkedBlockingQueue<>(2);

        // Producer thread
        Thread producer = new Thread(() -> {
            try {
                // Add items to the queue
                System.out.println("Producer: Adding item 1");
                queue.put("Item 1");
                System.out.println("Producer: Adding item 2");
                queue.put("Item 2");
                System.out.println("Producer: Adding item 3");
                queue.put("Item 3");  // This will block as the queue is full
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });

        // Consumer thread
        Thread consumer = new Thread(() -> {
            try {
                // Remove items from the queue
                System.out.println("Consumer: Removing " + queue.take());
                System.out.println("Consumer: Removing " + queue.take());
                System.out.println("Consumer: Removing " + queue.take());
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });

        producer.start();
        consumer.start();

        producer.join();
        consumer.join();
    }
}

Output:

Producer: Adding item 1
Producer: Adding item 2
Consumer: Removing Item 1
Producer: Adding item 3
Consumer: Removing Item 2
Consumer: Removing Item 3

In this example:

  • The producer thread adds items to the LinkedBlockingQueue. It blocks when attempting to add the third item because the queue is full (capacity is 2).
  • The consumer thread removes items from the queue. Once an item is removed, the producer thread is unblocked and can continue adding items.

Example 2: Using Unbounded LinkedBlockingQueue

If you don’t specify a capacity, the LinkedBlockingQueue becomes unbounded and can grow indefinitely. Here's an example:

import java.util.concurrent.LinkedBlockingQueue;

public class UnboundedQueueExample {
    public static void main(String[] args) throws InterruptedException {
        // Create an unbounded LinkedBlockingQueue
        LinkedBlockingQueue<String> queue = new LinkedBlockingQueue<>();

        // Producer thread
        Thread producer = new Thread(() -> {
            try {
                for (int i = 1; i <= 5; i++) {
                    System.out.println("Producer: Adding item " + i);
                    queue.put("Item " + i);  // No blocking, queue is unbounded
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });

        // Consumer thread
        Thread consumer = new Thread(() -> {
            try {
                for (int i = 1; i <= 5; i++) {
                    System.out.println("Consumer: Removing " + queue.take());
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });

        producer.start();
        consumer.start();

        producer.join();
        consumer.join();
    }
}

Output:

Producer: Adding item 1
Consumer: Removing Item 1
Producer: Adding item 2
Consumer: Removing Item 2
Producer: Adding item 3
Consumer: Removing Item 3
Producer: Adding item 4
Consumer: Removing Item 4
Producer: Adding item 5
Consumer: Removing Item 5

In this case, the LinkedBlockingQueue grows as needed, and the producer can add items without being blocked, while the consumer consumes them as they arrive.


Example 3: Using Fairness Policy

You can also create a LinkedBlockingQueue with a fairness policy, ensuring that threads are served in the order they requested access. Here’s an example of using the fairness parameter:

import java.util.concurrent.LinkedBlockingQueue;

public class FairQueueExample {
    public static void main(String[] args) throws InterruptedException {
        // Create a fair LinkedBlockingQueue with a capacity of 2
        LinkedBlockingQueue<String> queue = new LinkedBlockingQueue<>(2, true);

        // Producer thread
        Thread producer = new Thread(() -> {
            try {
                System.out.println("Producer: Adding item 1");
                queue.put("Item 1");
                System.out.println("Producer: Adding item 2");
                queue.put("Item 2");
                System.out.println("Producer: Adding item 3");
                queue.put("Item 3");  // This will block as the queue is full
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });

        // Consumer threads
        Thread consumer1 = new Thread(() -> {
            try {
                System.out.println("Consumer 1: Removing " + queue.take());
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });

        Thread consumer2 = new Thread(() -> {
            try {
                System.out.println("Consumer 2: Removing " + queue.take());
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });

        consumer1.start();
        consumer2.start();
        producer.start();

        producer.join();
        consumer1.join();
        consumer2.join();
    }
}

Output:

Producer: Adding item 1
Producer: Adding item 2
Consumer 1: Removing Item 1
Producer: Adding item 3
Consumer 2: Removing Item 2

In this case, because fairness is enabled, consumer 1 is served before consumer 2, even though they both started at roughly the same time.


When to Use LinkedBlockingQueue

The LinkedBlockingQueue is a great choice for:

  1. Producer-Consumer Problems: It’s ideal for scenarios where you have a producer generating data and a consumer consuming it, and you need to manage the flow of data efficiently.
  2. Thread Coordination: In situations where multiple threads need to wait for and access shared resources, LinkedBlockingQueue provides the necessary blocking mechanism.
  3. Unbounded Queues: If your application requires the queue to grow dynamically and you're not concerned about a fixed capacity, the unbounded nature of LinkedBlockingQueue can be very useful.
  4. Fair Resource Allocation: When fairness is important and you want to ensure that threads get access in the order they requested.