Zum Inhalt springen

LinkedTransferQueue und TransferQueue in Java (mit Beispiel)

Vier leuchtende Korallenkugeln hängen an Messinghaken an einer Stange vor Senfgelb; der fünfte Haken ist leer, seine Kugel liegt in einer Glasschale auf einem Stahlständer darunter

In diesem Artikel lernst du eine sehr spezielle Queue kennen: die LinkedTransferQueue. Dieser Artikel beschreibt deren Eigenschaften und zeigt dir anhand eines Beispiels, wie du diese Queue einsetzt.

Wir befinden uns nunmehr am untersten Punkt der Queue-Klassenhierarchie:

Klassenhierarchie der Queues und Deques im JDK (UML-Klassendiagramm), LinkedTransferQueue hervorgehoben: LinkedTransferQueue implementiert TransferQueue; TransferQueue erweitert BlockingQueue, BlockingQueue erweitert Queue, Queue erweitert Collection
LinkedTransferQueue implementiert TransferQueue

TransferQueue Interface

Wie im Klassendiagramm gut zu erkennen ist, ist java.util.concurrent.LinkedTransferQueue die einzige Klasse, die das Interface TransferQueue implementiert.

TransferQueue definiert zusätzliche Enqueue-Methoden, die nur dann erfolgreich ausgeführt werden können, wenn ein anderer Thread mit take() oder poll() das übergebene Element entgegennimmt:

  • transfer(E e) – übermittelt das Element an einen Thread, der in take() oder einem zeitbegrenzten poll() auf ein Element wartet. Wenn solch ein Thread nicht existiert, blockiert die Methode so lange, bis ein anderer Thread take() oder poll() aufruft. Das Element liegt dabei schon in der Queue – size(), peek() und der Iterator sehen es –, nur der aufrufende Thread wartet, bis es entnommen wurde.
  • tryTransfer(E e) – übermittelt das Element an einen Thread, der in take() oder einem zeitbegrenzten poll() auf ein Element wartet. Wenn solch ein Thread nicht existiert, gibt die Methode umgehend false zurück.
  • tryTransfer(E e, long timeout, TimeUnit unit) – übermittelt das Element an einen Thread, der in take() oder einem zeitbegrenzten poll() auf ein Element wartet. Wenn solch ein Thread nicht existiert und auch nicht innerhalb der Wartezeit erscheint, gibt die Methode false zurück.

Außerdem kannst du mit hasWaitingConsumer() und getWaitingConsumerCount() abfragen, ob bzw. wie viele Threads gerade in take() oder einem zeitbegrenzten poll() auf ein Element warten.

LinkedTransferQueue Eigenschaften

Bei der LinkedTransferQueue handelt es sich um eine unbounded blocking Queue, d. h. die regulären Enqueue-Operationen put() und offer() können nicht blockieren (da die Queue beliebig groß werden kann). Blockieren können hingegen:

  • die Dequeue-Operationen (wenn die Queue leer ist)
  • und die transfer()-Methode sowie die zeitbegrenzte tryTransfer()-Variante des TransferQueue-Interfaces, bis die jeweiligen Elemente entnommen werden.

LinkedTransferQueue basiert auf einer einfach verketteten Liste. Das hat zur Folge, dass die Zeitkomplexität der size()-Methode O(n) beträgt¹ (und nicht O(1) wie bei den Array-basierten Queues), da zum Bestimmen der Länge die komplette Liste abgelaufen werden muss.

Threadsicherheit wird durch nicht-blockierende Compare-and-set (CAS)-Operationen erreicht, was eine hohe Performance bei niedriger bis moderater Contention (Zugriffskonflikten durch mehrere Threads) sicherstellt.

Die Eigenschaften im Detail:

Unterliegende DatenstrukturThread-safe?Blocking/ Non-blockingFairness PolicyBounded/ UnboundedIterator Type
Verkettete ListeJa (optimistisches Locking durch Compare-and-set)BlockingNicht verfügbarUnboundedWeakly consistent²

¹ Alles über Zeitkomplexität erfährst du im Artikel „O-Notation und Zeitkomplexität – anschaulich erklärt“

² Weakly consistent: Alle Elemente, die zum Zeitpunkt der Erzeugung des Iterators in der Queue liegen, werden vom Iterator genau einmal durchlaufen. Änderungen, die danach erfolgen, können – müssen aber nicht – durch den Iterator berücksichtigt werden.

Einsatzempfehlung

Lange Zeit war die LinkedTransferQueue eine Randfigur des JDK: Die Klasse entstand 2009 zusammen mit dem Fork/Join-Framework und diente während dessen Entwicklung als Submission-Queue des ForkJoinPool. Noch vor dem Release von Java 7 ersetzte das Framework sie jedoch durch eigene Work-Queues. Seit Java 22 ist das anders: Die SynchronousQueue ist seitdem intern eine LinkedTransferQueue, und damit läuft jeder mit Executors.newCachedThreadPool() erzeugte Thread-Pool auf eben dieser Queue.

Die Klasse ist also erprobt und wird aktiv gepflegt – zuletzt behob Java 26 einen Fehler, bei dem poll() unter hoher Contention null zurückgab, obwohl die Queue nicht leer war (JDK-8371740).

Einsetzen solltest du die LinkedTransferQueue, wenn ein Producer wissen muss, dass sein Element abgenommen wurde – etwa beim Nachrichtenaustausch zwischen Threads mit Empfangsbestätigung –, und du trotzdem die Möglichkeit brauchst, andere Elemente mit put() ohne Warten einzureihen. Brauchst du nur das eine oder nur das andere, sind SynchronousQueue bzw. LinkedBlockingQueue die einfachere Wahl.

LinkedTransferQueue Beispiel

Im folgenden Beispiel (→ Code auf GitHub) werden zwei Threads gestartet, die LinkedTransferQueue.transfer() aufrufen. Danach wird ein Element direkt in die Queue geschrieben. Dann werden zwei weitere Threads gestartet, die transfer() aufrufen. Abschließend werden so lange Elemente aus der Queue entnommen, bis diese wieder leer ist.

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));
  }
}

Im Folgenden siehst du die Ausgabe des Programms (die Zeilen von main und den Transfer-Threads können von Lauf zu Lauf die Plätze tauschen):

[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 = []

Das folgende Diagramm zeigt den Ablauf im Zeitverlauf – eine Zeile pro Element, die Zeit läuft von links nach rechts:

Zeitverlauf des Beispiels: vier transfer()-Aufrufe blockieren, bis main das jeweilige Element mit take() entnimmt; put(3) kehrt sofort zurück
transfer() kehrt erst zurück, wenn ein Consumer genau dieses Element entnommen hat – put() kehrt sofort zurück

Zu Beginn wird zweimal transfer() aufgerufen – beide Aufrufe kehren nicht zurück, obwohl die Elemente bereits in der Queue liegen (queue = [1], dann queue = [1, 2]). Dann wird einmal put() aufgerufen, und dieser Aufruf kehrt sofort zurück. Danach folgen zwei weitere transfer()-Aufrufe, die ebenfalls blockieren.

Sobald main das erste Element entnimmt, kehrt transfer(1) zurück; mit der Entnahme der 2 kehrt transfer(2) zurück. Die Entnahme der 3 löst keine weitere Aktion aus, da diese mit put() in die Queue geschrieben wurde. Nach der Entnahme der 4 und der 5 kehren schließlich transfer(4) und transfer(5) zurück.

Zusammenfassung und Ausblick

In diesem Artikel hast du das TransferQueue-Interface und die LinkedTransferQueue-Implementierung kennengelernt und an einem Beispiel gesehen, wie du sie einsetzt.

Einen Vergleich aller Queue-Implementierungen des JDK und eine Übersicht, in welchen Fällen du welche Implementierung verwenden solltest, findest du im Artikel über das Queue-Interface.

Im nächsten Teil dieser Tutorialserie zeige ich dir, wie du Queues selbst implementieren kannst, beginnend mit der Implementierung mit einem Stack.

Wenn dir dieser Artikel gefallen hat, freue ich mich über eine Bewertung auf meinem ProvenExpert-Profil.

👉 Bewertung abgeben

Datenstrukturen wirklich verstehen?

Dieser Artikel zeigt eine Datenstruktur. „Mastering Data Structures in Java“ zeigt dir alle – und vor allem, wann du welche einsetzt und warum das über die Laufzeit deiner Anwendung entscheidet.

Der Premium-Online-Kurs ist gerade geschlossen. Auf der Warteliste erfährst du als Erste:r, wenn er wieder öffnet – und bekommst das Angebot vor allen anderen.

Zur Warteliste

Werde ein:e bessere:r Java-Entwickler:in

Mit meinem kostenlosen Newsletter bleibst du vorn. Modernes Java: neue Versionen & Features, Performance und JVM-Insights – 1x im Monat.

Suche