Ein Handbuch zu Java SynchronousQueue

Eine Anleitung zu Java SynchronousQueue

1. Überblick

In diesem Artikel werden dieSynchronousQueue aus demjava.util.concurrent-Paket betrachtet.

Einfach ausgedrückt, diese Implementierung ermöglicht es uns, Informationen zwischen Threads auf thread-sichere Weise auszutauschen.

2. API-Übersicht

DasSynchronousQueue hat nurtwo supported operations: take() and put(), and both of them are blocking.

Wenn wir beispielsweise der Warteschlange ein Element hinzufügen möchten, müssen wir die Methodeput()aufrufen. Diese Methode wird blockiert, bis ein anderer Thread die Methodetake()aufruft und signalisiert, dass sie bereit ist, ein Element aufzunehmen.

ObwohlSynchronousQueue eine Schnittstelle einer Warteschlange hat, sollten wir es als Austauschpunkt für ein einzelnes Element zwischen zwei Threads betrachten, in dem ein Thread ein Element übergibt und ein anderer Thread dieses Element übernimmt.

3. Implementieren von Übergaben mithilfe einer gemeinsam genutzten Variablen

Um zu sehen, warumSynchronousQueue so nützlich sein können, implementieren wir eine Logik unter Verwendung einer gemeinsam genutzten Variablen zwischen zwei Threads. Als Nächstes schreiben wir diese Logik mitSynchronousQueue neu, um unseren Code viel einfacher und lesbarer zu machen.

Angenommen, wir haben zwei Threads - einen Produzenten und einen Konsumenten. Wenn der Produzent einen Wert für eine gemeinsam genutzte Variable festlegt, möchten wir diese Tatsache dem Konsumententhread signalisieren. Als Nächstes ruft der Consumer-Thread einen Wert aus einer gemeinsam genutzten Variablen ab.

Wir werdenCountDownLatch verwenden, um diese beiden Threads zu koordinieren, um zu verhindern, dass der Verbraucher auf einen Wert einer gemeinsam genutzten Variablen zugreift, der noch nicht festgelegt wurde.

Wir definieren einesharedState-Variable und eineCountDownLatch-Variable, die zur Koordinierung der Verarbeitung verwendet werden:

ExecutorService executor = Executors.newFixedThreadPool(2);
AtomicInteger sharedState = new AtomicInteger();
CountDownLatch countDownLatch = new CountDownLatch(1);

Der Produzent speichert eine zufällige Ganzzahl in der VariablensharedState und führt die MethodecountDown() für diecountDownLatch, aus, die dem Verbraucher signalisiert, dass er einen Wert aussharedState: abrufen kann

Runnable producer = () -> {
    Integer producedElement = ThreadLocalRandom
      .current()
      .nextInt();
    sharedState.set(producedElement);
    countDownLatch.countDown();
};

Der Verbraucher wartet mit der Methodeawait()auf diecountDownLatch. Wenn der Produzent signalisiert, dass die Variable gesetzt wurde, ruft der Konsument sie aussharedState: ab

Runnable consumer = () -> {
    try {
        countDownLatch.await();
        Integer consumedElement = sharedState.get();
    } catch (InterruptedException ex) {
        ex.printStackTrace();
    }
};

Zu guter Letzt starten wir unser Programm:

executor.execute(producer);
executor.execute(consumer);

executor.awaitTermination(500, TimeUnit.MILLISECONDS);
executor.shutdown();
assertEquals(countDownLatch.getCount(), 0);

Es wird die folgende Ausgabe erzeugen:

Saving an element: -1507375353 to the exchange point
consumed an element: -1507375353 from the exchange point

Wir können sehen, dass dies eine Menge Code ist, um eine so einfache Funktionalität wie das Austauschen eines Elements zwischen zwei Threads zu implementieren. Im nächsten Abschnitt werden wir versuchen, es besser zu machen.

4. Implementieren von Handoffs mitSynchronousQueue

Lassen Sie uns jetzt die gleiche Funktionalität wie im vorherigen Abschnitt implementieren, jedoch mit einemSynchronousQueue.. Dies hat einen doppelten Effekt, da wir es zum Austauschen des Status zwischen Threads und zum Koordinieren dieser Aktion verwenden können, sodass wir nichts verwenden müssen nebenSynchronousQueue.

Zunächst definieren wir eine Warteschlange:

ExecutorService executor = Executors.newFixedThreadPool(2);
SynchronousQueue queue = new SynchronousQueue<>();

Der Produzent ruft eineput()-Methode auf, die blockiert, bis ein anderer Thread ein Element aus der Warteschlange nimmt:

Runnable producer = () -> {
    Integer producedElement = ThreadLocalRandom
      .current()
      .nextInt();
    try {
        queue.put(producedElement);
    } catch (InterruptedException ex) {
        ex.printStackTrace();
    }
};

Der Verbraucher ruft dieses Element einfach mit der Methodetake()ab:

Runnable consumer = () -> {
    try {
        Integer consumedElement = queue.take();
    } catch (InterruptedException ex) {
        ex.printStackTrace();
    }
};

Als nächstes starten wir unser Programm:

executor.execute(producer);
executor.execute(consumer);

executor.awaitTermination(500, TimeUnit.MILLISECONDS);
executor.shutdown();
assertEquals(queue.size(), 0);

Es wird die folgende Ausgabe erzeugen:

Saving an element: 339626897 to the exchange point
consumed an element: 339626897 from the exchange point

Wir können sehen, dass einSynchronousQueue als Austauschpunkt zwischen den Threads verwendet wird, was viel besser und verständlicher ist als das vorherige Beispiel, in dem der gemeinsame Zustand zusammen mit einemCountDownLatch. verwendet wurde

5. Fazit

In diesem kurzen Tutorial haben wir uns das KonstruktSynchronousQueueangesehen. Wir haben ein Programm erstellt, das Daten zwischen zwei Threads unter Verwendung des gemeinsam genutzten Status austauscht, und dieses Programm dann neu geschrieben, um das KonstruktSynchronousQueuezu nutzen. Dies dient als Austauschpunkt, der den Erzeuger- und den Verbraucher-Thread koordiniert.

Die Implementierung all dieser Beispiele und Codefragmente finden Sie inGitHub project - dies ist ein Maven-Projekt, daher sollte es einfach zu importieren und auszuführen sein, wie es ist.