Skip to content

Java LinkedBlockingQueue: Blocking Queue With Two Locks

Six glowing spheres connected by short rods in a row between two brushed-steel turnstiles against purple; on the left, a seventh sphere waits in front of the locked turnstile, on the right the turnstile rotates around the first sphere

This part of the tutorial series is about LinkedBlockingQueue. You will get to know its unique characteristics and see how to use this queue with an example. You will also learn when exactly you should use this queue.

Here we are in the class hierarchy:

Class hierarchy of the queues and deques in the JDK (UML class diagram), LinkedBlockingQueue highlighted: LinkedBlockingQueue implements BlockingQueue; BlockingQueue extends Queue, Queue extends Collection
LinkedBlockingQueue implements BlockingQueue

LinkedBlockingQueue Characteristics

The class java.util.concurrent.LinkedBlockingQueue is – just like ConcurrentLinkedQueue – based on a linked list, but is – like ArrayBlockingQueue presented in the next part – thread-safe (see below), blocking, and optionally bounded.

Unlike ArrayBlockingQueue, LinkedBlockingQueue does not provide a fairness policy. (Fairness policy means that blocking methods are served in the order they were called.)

Whether the queue is bounded is decided when you create it: new LinkedBlockingQueue<>() creates a queue with the capacity Integer.MAX_VALUE – practically unbounded. new LinkedBlockingQueue<>(capacity) sets a limit; once it is reached, put() blocks, and offer() returns false.

null elements are not allowed (offer(null) throws a NullPointerException) – which is why poll() can unambiguously signal “queue empty” with the return value null.

The queue’s characteristics in detail:

Underlying data structureThread-safe?Blocking/ non-blockingFairness policyBounded/ unboundedIterator type
Linked listYes (pessimistic locking with two locks)BlockingNot availableOptionally bounded (default: unbounded)Weakly consistent¹

¹ Weakly consistent: Elements that remain in the queue without interruption since the iterator was created are returned by the iterator exactly once. Changes after its creation – insertions as well as removals – may be reflected by the iterator, but need not be; it never throws a ConcurrentModificationException.

The iterator supports remove().

I recommend LinkedBlockingQueue if you need a blocking, thread-safe queue.

Compared to ArrayBlockingQueue, it has the higher throughput in most cases because producers and consumers rarely block each other – how that works is shown in the section “Is LinkedBlockingQueue thread-safe?”. In return, it allocates a node of its own for every element, and according to the Javadoc, its performance is less predictable than that of an array-based queue in most concurrent applications.

ArrayBlockingQueue is the better choice if you need a fairness policy or if even latencies matter more to you than throughput. If nothing should block at all, use ConcurrentLinkedQueue.

LinkedBlockingQueue Example

The following example shows how to use LinkedBlockingQueue. We create a queue with a capacity of 3. Immediately afterward, we start reading elements from the queue at intervals of three seconds. After 3.5 seconds, we begin writing elements to the queue at intervals of one second each (→ code on GitHub).

public class LinkedBlockingQueueExample {
  private static final long startTime = System.currentTimeMillis();

  static void main() throws InterruptedException {
    BlockingQueue<Integer> queue = new LinkedBlockingQueue<>(3);
    ScheduledExecutorService pool = Executors.newScheduledThreadPool(10);

    // Start reading from the queue immediately, every 3 seconds
    for (int i = 0; i < 10; i++) {
      int delaySeconds = i * 3;
      pool.schedule(() -> dequeue(queue), delaySeconds, TimeUnit.SECONDS);
    }

    // Start writing to the queue after 3.5 seconds (so there are already 2 threads
    // waiting), every second (so that the queue fills faster than it's emptied,
    // so that we see a full queue soon)
    for (int i = 0; i < 10; i++) {
      int element = i; // Assign to an effectively final variable
      int delayMillis = 3500 + i * 1000;
      pool.schedule(
          () -> enqueue(queue, element), delayMillis, TimeUnit.MILLISECONDS);
    }

    pool.shutdown();
    pool.awaitTermination(1, TimeUnit.MINUTES);
  }

  private static void enqueue(BlockingQueue<Integer> queue, int element) {
    log("Calling queue.put(%d) (queue = %s)...", element, queue);
    try {
      queue.put(element);
      log("queue.put(%d) returned (queue = %s)", element, queue);
    } catch (InterruptedException e) {
      Thread.currentThread().interrupt();
    }
  }

  private static void dequeue(BlockingQueue<Integer> queue) {
    log("    Calling queue.take() (queue = %s)...", queue);
    try {
      Integer element = queue.take();
      log("    queue.take() returned %d (queue = %s)", element, queue);
    } catch (InterruptedException e) {
      Thread.currentThread().interrupt();
    }
  }

  private static void log(String format, Object... args) {
    System.out.printf(
        Locale.US,
        "[%4.1fs] [%-16s] %s%n",
        (System.currentTimeMillis() - startTime) / 1000.0,
        Thread.currentThread().getName(),
        String.format(format, args));
  }
}

Below you can see the output of the sample program:

[ 0.0s] [pool-1-thread-1 ]     Calling queue.take() (queue = [])...
[ 3.0s] [pool-1-thread-4 ]     Calling queue.take() (queue = [])...
[ 3.5s] [pool-1-thread-8 ] Calling queue.put(0) (queue = [])...
[ 3.5s] [pool-1-thread-1 ]     queue.take() returned 0 (queue = [])
[ 3.5s] [pool-1-thread-8 ] queue.put(0) returned (queue = [])
[ 4.5s] [pool-1-thread-5 ] Calling queue.put(1) (queue = [])...
[ 4.5s] [pool-1-thread-4 ]     queue.take() returned 1 (queue = [])
[ 4.5s] [pool-1-thread-5 ] queue.put(1) returned (queue = [])
[ 5.5s] [pool-1-thread-3 ] Calling queue.put(2) (queue = [])...
[ 5.5s] [pool-1-thread-3 ] queue.put(2) returned (queue = [2])
[ 6.0s] [pool-1-thread-7 ]     Calling queue.take() (queue = [2])...
[ 6.0s] [pool-1-thread-7 ]     queue.take() returned 2 (queue = [])
[ 6.5s] [pool-1-thread-9 ] Calling queue.put(3) (queue = [])...
[ 6.5s] [pool-1-thread-9 ] queue.put(3) returned (queue = [3])
[ 7.5s] [pool-1-thread-6 ] Calling queue.put(4) (queue = [3])...
[ 7.5s] [pool-1-thread-6 ] queue.put(4) returned (queue = [3, 4])
[ 8.5s] [pool-1-thread-2 ] Calling queue.put(5) (queue = [3, 4])...
[ 8.5s] [pool-1-thread-2 ] queue.put(5) returned (queue = [3, 4, 5])
[ 9.0s] [pool-1-thread-10]     Calling queue.take() (queue = [3, 4, 5])...
[ 9.0s] [pool-1-thread-10]     queue.take() returned 3 (queue = [4, 5])
[ 9.5s] [pool-1-thread-1 ] Calling queue.put(6) (queue = [4, 5])...
[ 9.5s] [pool-1-thread-1 ] queue.put(6) returned (queue = [4, 5, 6])
[10.5s] [pool-1-thread-8 ] Calling queue.put(7) (queue = [4, 5, 6])...
[11.5s] [pool-1-thread-4 ] Calling queue.put(8) (queue = [4, 5, 6])...
[12.0s] [pool-1-thread-5 ]     Calling queue.take() (queue = [4, 5, 6])...
[12.0s] [pool-1-thread-5 ]     queue.take() returned 4 (queue = [5, 6, 7])
[12.0s] [pool-1-thread-8 ] queue.put(7) returned (queue = [5, 6, 7])
[12.5s] [pool-1-thread-3 ] Calling queue.put(9) (queue = [5, 6, 7])...
[15.0s] [pool-1-thread-7 ]     Calling queue.take() (queue = [5, 6, 7])...
[15.0s] [pool-1-thread-7 ]     queue.take() returned 5 (queue = [6, 7, 8])
[15.0s] [pool-1-thread-4 ] queue.put(8) returned (queue = [6, 7, 8])
[18.0s] [pool-1-thread-9 ]     Calling queue.take() (queue = [6, 7, 8])...
[18.0s] [pool-1-thread-3 ] queue.put(9) returned (queue = [7, 8, 9])
[18.0s] [pool-1-thread-9 ]     queue.take() returned 6 (queue = [7, 8, 9])
[21.0s] [pool-1-thread-6 ]     Calling queue.take() (queue = [7, 8, 9])...
[21.0s] [pool-1-thread-6 ]     queue.take() returned 7 (queue = [8, 9])
[24.0s] [pool-1-thread-2 ]     Calling queue.take() (queue = [8, 9])...
[24.0s] [pool-1-thread-2 ]     queue.take() returned 8 (queue = [9])
[27.0s] [pool-1-thread-10]     Calling queue.take() (queue = [9])...
[27.0s] [pool-1-thread-10]     queue.take() returned 9 (queue = [])

Since we start writing only after two threads have already called take(), these first two read attempts block at 0.0 and 3.0 s (threads 1 and 4).

After 3.5 s, the first element is written (thread 8). This wakes up thread 1, and the take() method immediately removes this element from the queue again.

After 4.5 s, the second element is written (thread 5). Thread 4 is woken up and takes this element from the queue again.

The program writes faster than it reads. After 10.5 s, a writing thread (thread 8) blocks for the first time when trying to write 7 into the queue, which is full at that time. After 11.5 s, thread 4 also blocks when trying to write 8 into the queue.

After 12.0 s, thread 5 removes an element from the queue, which frees up space. Thread 8 is woken up and writes 7 into the queue.

See if you can read and understand the rest of the output yourself.

Is LinkedBlockingQueue Thread-Safe?

Yes, LinkedBlockingQueue is thread-safe.

Thread safety of LinkedBlockingQueue is guaranteed by pessimistic locking using two separate ReentrantLocks: putLock guards inserting at the end of the list, takeLock guards taking from the head. The number of elements is kept in an AtomicInteger named count, which both sides update – each under its own lock, which is why it is a non-blocking atomic addition rather than a plain counter.

So lock contention – one thread waiting for a lock the other holds – between producer and consumer threads is rare: only when a producer writes into an empty queue does it briefly take the takeLock to wake up a waiting consumer – and conversely, a consumer that reads from a full bounded queue briefly takes the putLock to wake up a waiting producer.

The following diagram shows the two locks at the two ends of the list – in the example, a queue holding the elements 3 and 4, into which a producer thread (on the right of the picture) writes 5 while a consumer thread (on the left) takes the first element:

A LinkedBlockingQueue with the nodes 3 and 4: on the left, a consumer thread calls take() and takes the takeLock, which guards taking at the head of the list; on the right, a producer thread calls put(5) and takes the putLock, which guards inserting at the end of the list; an arrow leads from each lock to the counter count below the list
Producer and consumer work at different ends of the list and take different locks – only the counter count is updated by both

Differences from other queues:

  • With ConcurrentLinkedQueue, thread safety is provided by optimistic locking via compare-and-set, resulting in better performance with low to moderate contention.
  • ArrayBlockingQueue is protected with only one ReentrantLock, so access conflicts between producer and consumer threads are possible.

LinkedBlockingQueue Time Complexity

As with all queues based on linked lists, the time required for enqueue and dequeue operations is independent of the length of the queue. The time complexity is, therefore, O(1).

That also applies to the size() method. Unlike ConcurrentLinkedQueue, which is also based on a linked list and runs through the complete list to count the elements each time size() is called, LinkedBlockingQueue uses the counter count, which is updated on insertion and removal, and thus keeps the size available in constant time.

Summary and Outlook

In this article, you have learned about LinkedBlockingQueue – a thread-safe, blocking, optionally bounded queue. It is thread-safe thanks to two locks – putLock at the end and takeLock at the head of the list – so producers and consumers rarely block each other. You saw an example of how you can use LinkedBlockingQueue, and you also learned in which cases you should use it.

LinkedBlockingQueue is based on a linked list. The next part of the tutorial is about the array-based counterpart – ArrayBlockingQueue.

Was this article helpful to you? Then I’d be happy if you took a moment to leave a review on my ProvenExpert profile.

👉 Leave a review

Want Even More Knowledge?

My blog features many articles on Java, software architecture, and performance — from foundational concepts to advanced patterns.

If you want to go deeper, check out my trainings: hands-on, easy to understand, and directly applicable to your day-to-day project work. Instead of theory, I teach principles that help you write code that is better, more maintainable, and more performant in the long run.

Explore the Java Trainings

Become a Better Java Developer

My free newsletter keeps you ahead. Modern Java: new versions & features, performance, and JVM insights – once a month.

Search