Leitfaden für java.util.concurrent.BlockingQueue

Anleitung zu java.util.concurrent.BlockingQueue

1. Überblick

In diesem Artikel werden wir uns eines der nützlichsten Konstruktejava.util.concurrent ansehen, um das gleichzeitige Produzent-Verbraucher-Problem zu lösen. Wir werden uns eine API derBlockingQueue-Schnittstelle ansehen und wie Methoden von dieser Schnittstelle das Schreiben gleichzeitiger Programme erleichtern.

Später in diesem Artikel werden wir ein Beispiel für ein einfaches Programm zeigen, das mehrere Producer-Threads und mehrere Consumer-Threads enthält.

2. BlockingQueue Typen

Wir können zwei Arten vonBlockingQueue unterscheiden:

  • unbegrenzte Warteschlange - kann fast unbegrenzt wachsen

  • Eingeschränkte Warteschlange - mit definierter maximaler Kapazität

2.1. Ungebundene Warteschlange

Das Erstellen unbegrenzter Warteschlangen ist einfach:

BlockingQueue blockingQueue = new LinkedBlockingDeque<>();

Die Kapazität vonblockingQueue wird aufInteger.MAX_VALUE. gesetzt. Alle Operationen, die der unbegrenzten Warteschlange ein Element hinzufügen, werden niemals blockiert, sodass sie sehr groß werden können.

Das Wichtigste beim Entwerfen eines Producer-Consumer-Programms unter Verwendung von BlockingQueue ohne Grenzen ist, dass Consumer Nachrichten so schnell verarbeiten können, wie Produzenten Nachrichten zur Warteschlange hinzufügen. Andernfalls könnte sich der Speicher füllen und wir würden eineOutOfMemory Ausnahme erhalten.

2.2. Begrenzte Warteschlange

Die zweite Art von Warteschlangen ist die begrenzte Warteschlange. Wir können solche Warteschlangen erstellen, indem wir die Kapazität als Argument an einen Konstruktor übergeben:

BlockingQueue blockingQueue = new LinkedBlockingDeque<>(10);

Hier haben wir einblockingQueue mit einer Kapazität von 10. Dies bedeutet, dass ein Verbraucher blockiert, wenn er versucht, ein Element zu einer bereits vollen Warteschlange hinzuzufügen, abhängig von einer Methode, mit der es hinzugefügt wurde (offer(),add() oderput()) bis Platz zum Einfügen eines Objekts verfügbar wird. Andernfalls schlagen die Vorgänge fehl.

Die Verwendung einer begrenzten Warteschlange ist eine gute Möglichkeit, gleichzeitig ablaufende Programme zu entwerfen, da beim Einfügen eines Elements in eine bereits volle Warteschlange die Vorgänge warten müssen, bis die Verbraucher aufholen und Speicherplatz in der Warteschlange zur Verfügung stellen. Es gibt uns Drosselung, ohne dass wir uns anstrengen müssen.

3. BlockingQueue API

Es gibt zwei Arten von Methoden in denBlockingQueue-Schnittstellen Methoden, die für das Hinzufügen von Elementen zu einer Warteschlange verantwortlich sind, sowie Methoden, die diese Elemente abrufen. Jede Methode aus diesen beiden Gruppen verhält sich anders, wenn die Warteschlange voll / leer ist.

3.1. Elemente hinzufügen

  • add() – gibttrue zurück, wenn das Einfügen erfolgreich war, andernfalls wird einIllegalStateException ausgelöst

  • put() – fügt das angegebene Element in eine Warteschlange ein und wartet bei Bedarf auf einen freien Steckplatz

  • offer() – gibttrue zurück, wenn das Einfügen erfolgreich war, andernfallsfalse

  • offer(E e, long timeout, TimeUnit unit) – versucht, ein Element in eine Warteschlange einzufügen und wartet innerhalb eines bestimmten Zeitlimits auf einen verfügbaren Steckplatz

3.2. Elemente abrufen

  • take() - wartet auf ein head-Element einer Warteschlange und entfernt es. Wenn die Warteschlange leer ist, wird sie blockiert und es wird darauf gewartet, dass ein Element verfügbar wird

  • poll(long timeout, TimeUnit unit) – ruft den Kopf der Warteschlange ab und entfernt ihn. Warten Sie gegebenenfalls bis zur angegebenen Wartezeit, bis ein Element verfügbar ist. Gibtnull nach einer Zeitüberschreitung zurück __

Diese Methoden sind die wichtigsten Bausteine ​​derBlockingQueue-Schnittstelle beim Erstellen von Produzenten-Verbraucher-Programmen.

4. Multithreaded Producer-Consumer-Beispiel

Erstellen wir ein Programm, das aus zwei Teilen besteht - einem Produzenten und einem Konsumenten.

Der Produzent erzeugt eine Zufallszahl von 0 bis 100 und gibt diese Zahl inBlockingQueue an. Wir haben 4 Producer-Threads und blockieren mit derput()-Methode, bis in der Warteschlange Speicherplatz verfügbar ist.

Wichtig ist, dass wir unsere Consumer-Threads davon abhalten, auf unbestimmte Zeit auf ein Element in einer Warteschlange zu warten.

Eine gute Technik, um vom Produzenten an den Verbraucher zu signalisieren, dass keine Nachrichten mehr zu verarbeiten sind, besteht darin, eine spezielle Nachricht zu senden, die als Giftpille bezeichnet wird. Wir müssen so viele Giftpillen versenden, wie wir Verbraucher haben. Wenn ein Verbraucher diese spezielle Giftpillen-Nachricht aus einer Warteschlange entnimmt, wird die Ausführung ordnungsgemäß abgeschlossen.

Schauen wir uns eine Produzentenklasse an:

public class NumbersProducer implements Runnable {
    private BlockingQueue numbersQueue;
    private final int poisonPill;
    private final int poisonPillPerProducer;

    public NumbersProducer(BlockingQueue numbersQueue, int poisonPill, int poisonPillPerProducer) {
        this.numbersQueue = numbersQueue;
        this.poisonPill = poisonPill;
        this.poisonPillPerProducer = poisonPillPerProducer;
    }
    public void run() {
        try {
            generateNumbers();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }

    private void generateNumbers() throws InterruptedException {
        for (int i = 0; i < 100; i++) {
            numbersQueue.put(ThreadLocalRandom.current().nextInt(100));
        }
        for (int j = 0; j < poisonPillPerProducer; j++) {
            numbersQueue.put(poisonPill);
        }
     }
}

Unser Erzeugerkonstruktor verwendet als Argument dieBlockingQueue, die zur Koordinierung der Verarbeitung zwischen Erzeuger und Verbraucher verwendet werden. Wir sehen, dass die MethodegenerateNumbers() 100 Elemente in eine Warteschlange stellt. Es dauert auch eine Giftpillen-Nachricht, um zu wissen, welche Art von Nachricht in eine Warteschlange eingereiht wird, wenn die Ausführung beendet ist. Diese Nachricht musspoisonPillPerProducer mal in eine Warteschlange gestellt werden.

Jeder Verbraucher nimmt ein Element aus einemBlockingQueue mit dertake()-Methode, sodass es blockiert, bis sich ein Element in einer Warteschlange befindet. Nachdem einInteger aus einer Warteschlange entnommen wurde, wird geprüft, ob es sich bei der Nachricht um eine Giftpille handelt. Wenn ja, ist die Ausführung eines Threads abgeschlossen. Andernfalls wird das Ergebnis zusammen mit dem Namen des aktuellen Threads in der Standardausgabe ausgedruckt.

Dies gibt uns einen Einblick in das Innenleben unserer Verbraucher:

public class NumbersConsumer implements Runnable {
    private BlockingQueue queue;
    private final int poisonPill;

    public NumbersConsumer(BlockingQueue queue, int poisonPill) {
        this.queue = queue;
        this.poisonPill = poisonPill;
    }
    public void run() {
        try {
            while (true) {
                Integer number = queue.take();
                if (number.equals(poisonPill)) {
                    return;
                }
                System.out.println(Thread.currentThread().getName() + " result: " + number);
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

Das Wichtigste ist die Verwendung einer Warteschlange. Wie im Producer-Konstruktor wird eine Warteschlange als Argument übergeben. Wir können dies tun, weilBlockingQueue ohne explizite Synchronisation zwischen Threads geteilt werden können.

Jetzt, da wir unseren Produzenten und Konsumenten haben, können wir unser Programm starten. Wir müssen die Kapazität der Warteschlange definieren und sie auf 100 Elemente festlegen.

Wir möchten 4 Produzenten-Threads haben und eine Anzahl von Konsumenten-Threads entspricht der Anzahl der verfügbaren Prozessoren:

int BOUND = 10;
int N_PRODUCERS = 4;
int N_CONSUMERS = Runtime.getRuntime().availableProcessors();
int poisonPill = Integer.MAX_VALUE;
int poisonPillPerProducer = N_CONSUMERS / N_PRODUCERS;
int mod = N_CONSUMERS % N_PRODUCERS;

BlockingQueue queue = new LinkedBlockingQueue<>(BOUND);

for (int i = 1; i < N_PRODUCERS; i++) {
    new Thread(new NumbersProducer(queue, poisonPill, poisonPillPerProducer)).start();
}

for (int j = 0; j < N_CONSUMERS; j++) {
    new Thread(new NumbersConsumer(queue, poisonPill)).start();
}

new Thread(new NumbersProducer(queue, poisonPill, poisonPillPerProducer + mod)).start();

BlockingQueue wird mit einem Konstrukt mit einer Kapazität erstellt. Wir schaffen 4 Produzenten und N Konsumenten. Wir geben unsere Giftpillenmeldung alsInteger.MAX_VALUEan, da dieser Wert von unserem Hersteller unter normalen Arbeitsbedingungen niemals gesendet wird. Das Wichtigste dabei ist, dassBlockingQueue verwendet wird, um die Arbeit zwischen ihnen zu koordinieren.

Wenn wir das Programm ausführen, setzen 4 Producer-Threads zufälligeIntegers inBlockingQueue und die Konsumenten nehmen diese Elemente aus der Warteschlange. Jeder Thread gibt den Namen des Threads zusammen mit einem Ergebnis als Standardausgabe aus.

5. Fazit

Dieser Artikel zeigt eine praktische Verwendung vonBlockingQueue und erläutert Methoden, die zum Hinzufügen und Abrufen von Elementen verwendet werden. Außerdem haben wir gezeigt, wie mitBlockingQueue ein Multithread-Programm für Produzenten und Konsumenten erstellt wird, um die Arbeit zwischen Produzenten und Konsumenten zu koordinieren.

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