Java TransferQueue and LinkedTransferQueue with Examples

Java TransferQueue and LinkedTransferQueue explained with transfer(), tryTransfer() and put() examples, timeouts, and a comparison with SynchronousQueue.

Three producer calls on a LinkedTransferQueue with no waiting consumer. put() queues the element and returns. transfer() queues the element and waits until a consumer calls take(). tryTransfer() returns false and does not queue the element.

In Java, TransferQueue is a BlockingQueue in which a producer can wait until a consumer has received its element, and LinkedTransferQueue is its only implementation in the JDK. The key method is transfer(), which returns only after a consumer has taken the element, whereas put() adds the element and returns at once.

We use a TransferQueue in message passing between threads when the producer sometimes needs proof of delivery, for example an order service that must know that the notification worker picked up the order before it answers the client. In other cases, the same queue works as a normal unbounded queue.

The following example shows the three ways to hand over an element when no consumer is waiting, with the result of each line as a comment.

LinkedTransferQueue<String> messages = new LinkedTransferQueue<>();
messages.put("order-1");                        // queued, returns at once
boolean handed = messages.tryTransfer("order-2");   // false (no consumer waiting, not queued)
boolean timed = messages.tryTransfer("order-3", 50, TimeUnit.MILLISECONDS);   // false after 50 ms, not queued
int queued = messages.size();                   // 1 (only order-1)
boolean waiting = messages.hasWaitingConsumer();    // false
String head = messages.poll();                  // "order-1"

Notice that tryTransfer() never leaves the element in the queue when it fails. We start with the interface and the LinkedTransferQueue class, move on to examples with real consumers, the constructors and methods, and end with a comparison with SynchronousQueue and other blocking queues.

1. TransferQueue Interface

The TransferQueue interface was added to the Java Collections Framework in Java 7, in the java.util.concurrent package. It extends BlockingQueue, so it has put(), take(), offer() and poll(), and it adds methods that hand an element to a consumer.

// does not compile: shortened declaration from the JDK, shown for reference
public interface TransferQueue<E> extends BlockingQueue<E> {
    void transfer(E e) throws InterruptedException;
    boolean tryTransfer(E e);
    boolean tryTransfer(E e, long timeout, TimeUnit unit) throws InterruptedException;
    boolean hasWaitingConsumer();
    int getWaitingConsumerCount();
}

When a consumer is already waiting in take() or a timed poll(), transfer() gives it the element at once. When no consumer is waiting, transfer() adds the element at the tail and blocks until a consumer takes it.

Three producer calls on a LinkedTransferQueue with no waiting consumer. put() queues the element and returns. transfer() queues the element and waits until a consumer calls take(). tryTransfer() returns false and does not queue the element.
With no waiting consumer, put() returns at once, transfer() waits for a consumer and tryTransfer() gives up and returns false
MethodConsumer is waitingNo consumer is waiting
put(e), offer(e), add(e)Element goes to the consumerElement is queued, the call returns
transfer(e)Element goes to the consumer, the call returnsElement is queued, the call waits until it is taken
tryTransfer(e)Element goes to the consumer, returns trueReturns false, element is not queued
tryTransfer(e, timeout, unit)Element goes to the consumer, returns trueWaits up to the timeout, returns false and removes the element

2. LinkedTransferQueue Class

The LinkedTransferQueue class implements TransferQueue with linked nodes and non-blocking compare-and-set updates instead of locks. Its rules are short.

  • The queue is unbounded, so put() and offer() never block and remainingCapacity() returns Integer.MAX_VALUE.
  • It orders elements FIFO with respect to any given producer.
  • Adding null throws a NullPointerException.
  • It is thread-safe, and its iterator is weakly consistent.
  • The method size() is not a constant-time operation, because it walks the nodes, and the result can be out of date while other threads change the queue.
  • Bulk operations such as addAll() and removeAll() are not atomic.

In the JDK 25 source code, SynchronousQueue is built on the internal code of LinkedTransferQueue, so both classes share the same handoff mechanism.

3. Java LinkedTransferQueue Example

The examples hand order confirmations from an order service to a notification worker. Each worker runs as a virtual thread, and the main thread waits for it with join(), so the results do not depend on timing.

3.1. Non-blocking Insertion and Removal

Without transfer(), a LinkedTransferQueue works like any unbounded FIFO queue. The methods offer() and poll() return at once.

LinkedTransferQueue<String> outbox = new LinkedTransferQueue<>(List.of("order-1"));
boolean offered = outbox.offer("order-2");      // true
String next = outbox.poll();                    // "order-1"
String last = outbox.poll();                    // "order-2"
String none = outbox.poll();                    // null

3.2. Blocking Transfer and Retrieval

In this example, the producer calls transfer() for three orders. Each call returns only after the worker has taken the order with take(), so the producer is never more than one order ahead of the worker.

LinkedTransferQueue<String> handoff = new LinkedTransferQueue<>();
List<String> notified = new CopyOnWriteArrayList<>();

Thread worker = Thread.ofVirtual().start(() -> {
    try {
        for (int i = 0; i < 3; i++) {
            notified.add(handoff.take());       // waits for the next order
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
});
handoff.transfer("order-1");                    // returns after the worker took order-1
handoff.transfer("order-2");
handoff.transfer("order-3");
int afterTransfers = handoff.size();            // 0 (every order was received)
worker.join();
List<String> received = List.copyOf(notified);  // [order-1, order-2, order-3]

The method tryTransfer() succeeds only when a consumer is already waiting. In the next snippet, the main thread waits until the worker blocks in take(), which hasWaitingConsumer() reports, before it tries the handoff.

LinkedTransferQueue<String> direct = new LinkedTransferQueue<>();
Thread listener = Thread.ofVirtual().start(() -> {
    try {
        direct.take();
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
});
while (!direct.hasWaitingConsumer()) {
    Thread.onSpinWait();                        // wait until the listener blocks in take()
}
int consumers = direct.getWaitingConsumerCount();   // 1
boolean delivered = direct.tryTransfer("order-9");  // true
listener.join();

4. Creating LinkedTransferQueue Instance

The class has two constructors, and neither takes a capacity.

  • LinkedTransferQueue() creates an empty queue.
  • LinkedTransferQueue(Collection c) creates a queue that contains the elements of the collection in iteration order.

When the code only needs the interface methods, we use TransferQueue<E> as the variable type, so the code reads as “a queue that supports transfer” and not as a specific class.

5. LinkedTransferQueue Methods

Besides the transfer methods in section 1, the class has the usual BlockingQueue methods. Only the retrieval methods wait, since an unbounded queue always has room.

MethodWhat it does
take()Removes the head, waits while the queue is empty
poll(timeout, unit)Removes the head, waits up to the timeout, returns null after it
poll(), peek()Return the head or null at once; poll() also removes it
hasWaitingConsumer()Returns true when at least one thread waits in take() or a timed poll()
getWaitingConsumerCount()Returns an estimate of the number of waiting consumers
drainTo(c), drainTo(c, max)Moves the queued elements into a collection
size()Counts the nodes, takes linear time

The count from getWaitingConsumerCount() is a snapshot that can change right after the call, so we use it for monitoring and not to decide whether a later transfer() will block.

6. Delivery With a Timeout and a Fallback

An order service answers the client within 200 ms. It tries to hand the confirmation to the notification worker, and when no worker takes it in time, it stores the confirmation in a retry list instead of blocking the request thread forever. The timed tryTransfer() fits that need.

TransferQueue<String> notifications = new LinkedTransferQueue<>();
List<String> retryLater = new ArrayList<>();
String confirmation = "order-42";
boolean pickedUp = notifications.tryTransfer(confirmation, 200, TimeUnit.MILLISECONDS);   // false (no worker running)
if (!pickedUp) {
    retryLater.add(confirmation);
}
List<String> retries = retryLater;              // [order-42]
int stuckInQueue = notifications.size();        // 0

The timed tryTransfer() removes the element when the timeout passes, so we keep our own copy for the retry. A plain transfer() has no timeout, so we use it only in background threads that may wait as long as the consumer needs.

7. LinkedTransferQueue vs SynchronousQueue vs LinkedBlockingQueue

The three classes cover a range from pure handoff to pure buffering. The LinkedTransferQueue sits in the middle, because each call decides whether to wait for a consumer.

AspectSynchronousQueueLinkedTransferQueueLinkedBlockingQueue
CapacityNoneUnboundedOptional bound
put() waits for a consumerAlwaysNeverOnly when full
Wait for delivery per callEvery callWith transfer()Not available
size()Always 0Linear timeConstant time
Typical useDirect handoff, thread poolsMixed handoff and bufferingClassic producer-consumer buffer

For a bounded buffer with backpressure, the ArrayBlockingQueue is the simpler choice. All of these classes implement the Queue interface, so code that only offers and polls can switch between them.

8. TransferQueue FAQs

Most questions about TransferQueue compare its methods with put() or ask how it differs from SynchronousQueue.

8.1. What Is the Difference Between transfer() and put()?

The method put() queues the element and returns, whereas transfer() returns only after a consumer has received the element. On a LinkedTransferQueue, put() never blocks, because the queue is unbounded.

8.2. What Does tryTransfer() Return When No Consumer Is Waiting?

The method returns false, and the element is not added to the queue. The timed version waits up to the timeout first and also leaves nothing in the queue when it fails, as the example in section 6 shows.

8.3. Is LinkedTransferQueue Bounded?

No. There is no capacity limit, so a producer that uses only put() or offer() can fill memory if consumers are slow. Using transfer() limits each producer thread to one element in flight.

8.4. When Should I Use LinkedTransferQueue Instead of SynchronousQueue?

We use LinkedTransferQueue when some messages need a handoff and others can wait in the queue. A SynchronousQueue forces a handoff for every element, while a LinkedTransferQueue lets each call choose between transfer() and put().

9. Conclusion

A TransferQueue adds delivery confirmation to a BlockingQueue. The method transfer() waits until a consumer has the element, tryTransfer() succeeds only with a waiting consumer, and the timed tryTransfer() gives up after a timeout without leaving the element behind.

The LinkedTransferQueue class is unbounded, lock-free and FIFO per producer, so it doubles as a normal queue when we call put(). We count on size() and getWaitingConsumerCount() only for monitoring, because both are estimates under concurrent use.

10. References

Happy Learning !!

Source Code on Github

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.