Skip to content

LinkedTransferQueue and TransferQueue in Java (+ Example)

Four glowing coral spheres hang from brass hooks on a rail against mustard yellow; the fifth hook is empty, its sphere rests in a glass bowl on a steel stand beneath it

In this article, you will learn about a very special queue: LinkedTransferQueue. This article describes its characteristics and shows you how to use this queue with an example.

We are now at the lowest point of the queue class hierarchy:

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

TransferQueue Interface

As you can see in the class diagram, java.util.concurrent.LinkedTransferQueue is the only class that implements the TransferQueue interface.

TransferQueue defines additional enqueue methods that can only be executed successfully if another thread takes over the transferred item using take() or poll():

  • transfer(E e) – passes the element to a thread that is waiting for an element in take() or a timed poll(). If such a thread does not exist, the method blocks until another thread calls take() or poll(). The element is already in the queue in the meantime – size(), peek(), and the iterator see it – only the calling thread waits until it has been taken.
  • tryTransfer(E e) – passes the element to a thread that is waiting for an element in take() or a timed poll(). If such a thread does not exist, the method immediately returns false.
  • tryTransfer(E e, long timeout, TimeUnit unit) – passes the element to a thread that is waiting for an element in take() or a timed poll(). If such a thread does not exist and does not appear within the waiting time, the method returns false.

In addition, hasWaitingConsumer() and getWaitingConsumerCount() tell you whether, and how many, threads are currently waiting for an element in take() or a timed poll().

LinkedTransferQueue Characteristics

LinkedTransferQueue is an unbounded blocking queue, i.e., the regular enqueue operations put() and offer() cannot block (since the queue can grow to any size). The following, however, can block:

  • the dequeue operations (when the queue is empty),
  • and the transfer() method as well as the timed tryTransfer() variant of the TransferQueue interface, until the respective elements have been taken.

LinkedTransferQueue is based on a singly linked list. As a result, the time complexity of the size() method is O(n) (and not O(1) as in the array-based queues)¹, since the entire list must be traversed to determine its length.

Thread safety is achieved through non-blocking compare-and-set (CAS) operations, ensuring high performance with low to moderate contention (access conflicts through multiple threads).

The characteristics in detail:

Underlying data structureThread-safe?Blocking/ non-blockingFairness policyBounded/ unboundedIterator type
Linked listYes (optimistic locking through compare-and-set)BlockingNot availableUnboundedWeakly consistent²

¹ You can learn about time complexity in the article “Big O Notation and Time Complexity – Easily Explained”.

² Weakly consistent: All elements that exist when the iterator is created are traversed by the iterator exactly once. Changes that occur after this can, but do not need to, be reflected by the iterator.

For a long time, LinkedTransferQueue was a marginal figure in the JDK: The class was created in 2009 together with the fork/join framework and served as the submission queue of ForkJoinPool during its development. Before Java 7 was released, however, the framework replaced it with its own work queues. Since Java 22, that has changed: SynchronousQueue is now internally a LinkedTransferQueue, which means that every thread pool created with Executors.newCachedThreadPool() runs on this very queue.

So the class is proven and actively maintained – most recently, Java 26 fixed a bug where poll() returned null under high contention even though the queue was not empty (JDK-8371740).

You should use LinkedTransferQueue when a producer needs to know that its element has been taken – for example, when exchanging messages between threads with receipt confirmation – and you still need the option to enqueue other elements with put() without waiting. If you need only one or the other, SynchronousQueue or LinkedBlockingQueue is the simpler choice.

LinkedTransferQueue Example

In the following example (→ code on GitHub), we start two threads that call LinkedTransferQueue.transfer(). After that, one element is written directly to the queue. Then, we create two more threads that call transfer(). Finally, we remove elements from the queue until it is empty again.

public class LinkedTransferQueueExample {
  static void main() throws InterruptedException {
    TransferQueue<Integer> queue = new LinkedTransferQueue<>();

    // Start 2 threads calling queue.transfer(),
    startTransferThread(queue, 1);
    startTransferThread(queue, 2);

    // ... then put one element directly,
    enqueueViaPut(queue, 3);

    // ... then start 2 more threads calling queue.transfer().
    startTransferThread(queue, 4);
    startTransferThread(queue, 5);

    // Now take all elements until the queue is empty
    while (!queue.isEmpty()) {
      dequeueViaTake(queue);
    }
  }

  private static void startTransferThread(TransferQueue<Integer> queue, int element)
      throws InterruptedException {
    new Thread(() -> enqueueViaTransfer(queue, element)).start();

    // Wait a bit to let the thread enqueue the element
    Thread.sleep(100);
    log("                            --> queue = " + queue);
  }

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

  private static void enqueueViaPut(TransferQueue<Integer> queue, int element)
      throws InterruptedException {
    log("Calling queue.put(%d)...", element);
    queue.put(element);
    log("queue.put(%d) returned       --> queue = %s", element, queue);
  }

  private static void dequeueViaTake(TransferQueue<Integer> queue)
      throws InterruptedException {
    log("    Calling queue.take() (queue = %s)...", queue);
    Integer e = queue.take();
    log("    queue.take() returned %d --> queue = %s", e, queue);

    // Wait a bit to get the log output in a readable order
    Thread.sleep(10);
  }

  private static void log(String format, Object... args) {
    System.out.printf(
        Locale.US, "[%-8s] %s%n",
        Thread.currentThread().getName(),
        String.format(format, args));
  }
}

Below you can see the output of the program (the lines of main and the transfer threads may swap places from run to run):

[Thread-0] Calling queue.transfer(1)...
[main    ]                             --> queue = [1]
[Thread-1] Calling queue.transfer(2)...
[main    ]                             --> queue = [1, 2]
[main    ] Calling queue.put(3)...
[main    ] queue.put(3) returned       --> queue = [1, 2, 3]
[Thread-2] Calling queue.transfer(4)...
[main    ]                             --> queue = [1, 2, 3, 4]
[Thread-3] Calling queue.transfer(5)...
[main    ]                             --> queue = [1, 2, 3, 4, 5]
[main    ]     Calling queue.take() (queue = [1, 2, 3, 4, 5])...
[main    ]     queue.take() returned 1 --> queue = [2, 3, 4, 5]
[Thread-0] queue.transfer(1) returned  --> queue = [2, 3, 4, 5]
[main    ]     Calling queue.take() (queue = [2, 3, 4, 5])...
[main    ]     queue.take() returned 2 --> queue = [3, 4, 5]
[Thread-1] queue.transfer(2) returned  --> queue = [3, 4, 5]
[main    ]     Calling queue.take() (queue = [3, 4, 5])...
[main    ]     queue.take() returned 3 --> queue = [4, 5]
[main    ]     Calling queue.take() (queue = [4, 5])...
[main    ]     queue.take() returned 4 --> queue = [5]
[Thread-2] queue.transfer(4) returned  --> queue = [5]
[main    ]     Calling queue.take() (queue = [5])...
[main    ]     queue.take() returned 5 --> queue = []
[Thread-3] queue.transfer(5) returned  --> queue = []

The following diagram shows the sequence over time – one row per element, time runs from left to right:

Timeline of the example: four transfer() calls block until main takes the respective element with take(); put(3) returns immediately
transfer() does not return until a consumer has taken exactly this element – put() returns immediately

In the beginning, transfer() is called twice – neither call returns, although the elements are already in the queue (queue = [1], then queue = [1, 2]). Then put() is called once, and this call returns immediately. Two more transfer() calls follow, which block as well.

As soon as main takes the first element, transfer(1) returns; with the removal of the 2, transfer(2) returns. The removal of the 3 does not trigger any further action, since it was written to the queue with put(). After removing the 4 and the 5, transfer(4) and transfer(5) finally return.

Summary and Outlook

In this article, you learned about the TransferQueue interface and the LinkedTransferQueue implementation and saw how to use them with an example.

In the article about the Queue interface, you will find a comparison of all queue implementations of the JDK and an overview of in which cases you should use which implementation.

In the next part of this tutorial series, I’ll show you how to implement a queue yourself, starting with implementing a queue using a stack.

If you enjoyed this article, I’d appreciate 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