So funktioniert Multithreading, Concurrency und paralleles Programmieren in Java.

Keylearnings:

  • Was ist der Unterschied zwischen Multithreading (Concurrency) und Parallelität?
  • Was ist der Unterschied zwischen Prozessen und Threads?
  • Wie funktioniert das Runnable Interface?
  • Wie du mit Hilfe des Schlüsselwortes join auf Threads wartest.
  • Was bewirkt das Schlüsselwort volatile?
  • Was sind Deadlocks und Livelocks?
  • Wie du mit Hilfe des Schlüsselworts synchronize mehrere Threads synchronisierst.
  • Was sind atomare Operationen?
  • Wie funktionieren die Schlüsselwörter wait und notify?

Lieber riskiere ich alles fallen zu lassen als zweimal zu gehen!

Letztes Jahr nach der Senioren-Weihnachtsfeier bei meiner Oma wollte ich alle Kaffeetassen in einem Rutsch in die Spülmaschine räumen.

Das ging gründlich schief und plötzlich hatte meine Oma nicht mehr alle Tassen im Schrank.

Auch dein Computer möchte möglichst viel gleichzeitig machen und muss dabei aufpassen das Geschirr nicht fallen zu lassen.

Und wie er das macht, wollen wir uns in diesem Artikel einmal genauer ansehen.

Der Themenbereich, der sich mit dieser Fragestellung beschäftigt ist das bzw. das parallele Programmieren Multithreading oder auch Concurrency genannt.

Vor allem wollen wir klären wie das Geschirr eines Computers aussieht.

Die Teller heißen hier Prozesse und die Tassen nennen wir Threads.

Parallele und sequentielle Programmierung

Das Gegenteil von paralleler Programmierung ist die sequentielle Programmierung.

Bei der sequentiellen Programmierung wird ein Programm synchron abgearbeitet, d.h. jede Zeile wird der Reihe nach ausgeführt.

Wohingegen in der parallelen Programmierung verschiedene Programmteile zur selben Zeit verarbeitet werden.

Aufgrund der heute üblichen Mehrkern-Prozessoren ist eine echte Parallelität möglich.

Parallelität und Nebenläufigkeit (Concurrency)

Jeder Prozessor-Kern kann einen sogenannten Prozess abarbeiten.

Hat dein Prozessor also z.B. vier Kerne, so können vier Prozesse gleichzeitig abgearbeitet werden.

Genau das nennen wir Parallelität!

Aber was passiert, sobald mehr Prozesse laufen als dein Prozessor Kerne hat? Lässt dein Rechner dann das Geschirr fallen?

Nein noch nicht! Denn in diesem Fall betritt die sogenannte Nebenläufigkeit, oder wie der Fachmann sagt, die Concurrency das Spielfeld.

Bei der Nebenläufigkeit oder der Concurrency werden den einzelnen Prozessen Time-Slots zugewiesen, in denen diese die vorhandenen Prozessorkerne verwenden dürfen.

Natürlich sollten diese Time-Slots so optimal gewählt werden, dass es für den Anwender den Anschein hat, als würden alle Prozesse gleichzeitig abgearbeitet werden.

Prozesse und Threads

Ein Prozess können wir uns als eine Ausführungseinheit eines Programmes vorstellen.

Die Planung und die Verwaltung der Prozesse geschieht durch den Prozess-Scheduler des Betriebssystems.

Jeder Prozess besitzt alles was zu seiner Ausführung benötigt wird. So hat jeder Prozess beispielsweise seinen eigenen Speicherbereich reserviert.

Verschiedene Prozesse können miteinander reden und das sogar System übergreifend. Diese Tatsache macht das heutige Cloud-Computing erst möglich.

Die Kommunikation zwischen unterschiedlichen Prozessen nennt man Inter-Process-Communication (IPC).Ein Prozess enthält wiederum ein oder mehrere Threads.

Process

Threads funktionieren ähnlich wie die Prozesse selber, nur dass ein Thread die Ressourcen des Prozesses verwendet zu dem er gehört.

Mit Hilfe von Threads können wir Nebenläufigkeit innerhalb eines Prozesses erreichen.

Wir können uns Threads also als eine Unterteilung eines Prozesses in Einheiten vorstellen, welche Nebenläufig (in Concurrency) auf den zu diesem Prozess gehörenden Ressourcen ausgeführt werden.

Da unterschiedliche Threads die gleichen Systemressourcen verwenden, kann es zu Ressourcen Konflikten kommen, die wir beachten und behandeln müssen.

Vor- und Nachteile von Multithreading und paralleler Programmierung

Ist die Welt der parallelen Programmierung und des Multithreading ein Ponyhof bei gutem Wetter?

Oder gibt es auch dunkle Wolken, stark Regen und viel Matsch?

Natürlich können wir aufgrund des Multithreading Programme schreiben, welche die zur Verfügung stehenden Ressourcen besser ausnutzen, daher performanter sind und auf mehrere Dinge gleichzeitig reagieren können. Was wiederum zu einer besseren User Experience führt.

Aber gibt es auch Nachteile? Leider lautet die Antwort Ja!

Die Entwicklung von Programmen, welche Multithreading oder parallele Programmierung verwenden ist weit komplexer.

Wegen der gemeinsamen Nutzung des selben Speicherbereichs durch verschiedene Threads können sehr schnell, sehr schwer aufzufindene Fehler auftreten.

Da die Terminierung, also die Zeit-Slots, in denen ein Thread bestimmte Ressourcen verwenden darf, bei jedem Programmdurchlauf unterschiedlich sein kann, sind diese Art der Fehler häufig nicht reproduzierbar und treten nur sporadisch auf.

Und natürlich ist auch die Koordination und Organisation der Threads nicht umsonst zu haben.

Sobald wir es mit dem Multithreading (Concurrency) übertreiben sind diese Kosten sogar so hoch, dass der potentielle Leistungsgewinn, den wir durch die Parallelisierung erreichen zu nichte gemacht wird.

Die Lebensgeschichte eines Threads in JAVA

Jetzt hol dir am besten eine Tüte Chips, setz dich hin und hör mir zu. Ich möchte dir über das Leben eines Threads in Java erzählen.

Ein Thread kennt fünf Phasen.

Wie bei uns allen erblickt auch ein Thread das Licht der Welt durch seine Geburt. Diese Phase nennen wir new.

Lifecycle Thread

Sobald er dann gesund und munter ist, ist er auch bereit Arbeit zu übernehmen. Was für ein fleißiges Kerlchen! Nicht wahr? Diese Phase heißt Runnable.

In dem Moment wo der Thread Arbeit bekommt, wechselt er in die Running Phase, in der er versucht seine Aufgaben so schnell wie möglich zu erledigen.

Da ein Thread mit anderen Threads kommuniziert kann es vorkommen, dass dieser auf ein Ergebnis eines Kollegen warten muss und in die Phase waiting wechselt. Diese Situation ist insbesondere im Zusammenhang mit dem Producer Consumer Pattern, das wir uns noch genauer ansehen werden, wichtig. Sobald die waiting Phase beendet ist geht ein Thread wieder zurück in den Running mode.

Nun leg deine Tüte Chips weg und hol dir ein Taschentuch, denn auch das Leben eines Threads endet mit dem Tod (Dead). Sobald ein Thread gestorben ist, steht dieser nicht mehr zur Verfügung.

Sequentielle und parallele Ausführung im Vergleich

Okay, machen wir uns die Finger dreckig und verwandeln ein sequentielles in ein parallel ausführbares Programm.

Zu diesem Zweck betrachten wir das folgende sequentielle Beispiel-Programm:

1: class Runner1 {

2:    public void startRunning() {
3:      for (int i=0;i<=10;i++) {
4:        System.out.println("Runner1 "+i);
5:      }
6:    }

7:}


8: class Runner2 {

9:  public void startRunning() {
10:    for (int i=0;i<=10;i++) {
11:      System.out.println("Runner2 "+i);
12:    }
13:  }

14:}

15: public class RunnerExample {

16:  public static void main(String[] args) {
17:    Runner1 runner1 = new Runner1();
18:    Runner2 runner2 = new Runner2();
19:    runner1.startRunning();
20:    runner2.startRunning();
21:  }

22:}

Das Beispielprogramm besteht aus den beiden analog aufgebauten Klassen Runner1 und Runner2.

Beide Klassen enthalten die Methode startRunning, in der innerhalb einer for Schleife, eine Bidlschirmausgabe erzeugt wird, anhand der erkennbar ist in welcher Klasse und in welchem Schleifendurchlauf die Ausgabe erzeugt wurde.

In der main Methode (ab Zeile 16) wird jeweils eine Instanz jeder Klasse erzeugt (Zeile 17 und 18) und für jede Instanz die Methode startRunning aufgerufen.

Lassen wir unser Programm laufen, so erhalten wir folgende Ausgabe:

Runner1 0
Runner1 1
Runner1 2
Runner1 3
Runner1 4
Runner1 5
Runner1 6
Runner1 7
Runner1 8
Runner1 9
Runner1 10
Runner2 0
Runner2 1
Runner2 2
Runner2 3
Runner2 4
Runner2 5
Runner2 6
Runner2 7
Runner2 8
Runner2 9
Runner2 10

Anhand dieser Ausgabe können wir sehr schön die sequentielle Abarbeitung erkennen.

Als erstes wird die Methode startRunning von Runner1 vollständig abgearbeitet und erst nach dem das vollständig abgeschlossen ist, startet die startRunning Methode aus Runner2.

Kümmern wir uns nun um die Parallelisierung.

Wir wollen erreichen, dass die Abarbeitung der Methode startRunning von Runner1 und Runner2 zur gleichen Zeit stattfindet.

Concurrency

Das Runnable Interface

Das JAVA Mittel, mit dem wir dieses Ziel erreichen können ist das Runnable Interface, das wir in unseren beiden Runner Klassen implementieren müssen.

Wir führen dieses Prozedre am Beispiel von Runner1 durch. Für die Klasse Runner2 ist das Vorgehen exakt das Selbe.

Für diesen Zweck haben wir genau zwei Dinge zu erledigen.

  1. Einbindung des Runnable Interface in die Runner Klasse.
  2. Überschreiben der run Methode aus dem Runnable Interface.

Danach können wir einfach den Code, der parallel ausgeführt werden soll, innerhalb der run() Methode platzieren. In unserem Beispiel rufen wir hier einfach die Methode startRunning() auf.

Fertig ist die Raketentechnik! Hier die angepasste Klasse Runner1.

1: class Runner1 implements Runnable{

2:    public void startRunning() {
3:      for (int i=0;i<=10;i++) {
4:        System.out.println("Runner1 "+i);
5:      }
6:    }

7:    @Override
8:    public void run() {
9:      startRunning();
10:    }
11:}

Jetzt fehlt nur noch eines.

Und zwar Threads, in denen unser Code ausgeführt wird.

Zu diesem Zweck müssen wir für jede unserer Runner Klassen einen Thread erzeugen.

Den Runner, um den sich der jeweilige Thread kümmern soll, können wir einfach als Konstruktor-Argument übergeben.

Realisiert sieht das ganze dann folgendermaßen aus:

Thread t1 = new Thread(new Runner1());
Thread t2 = new Thread(new Runner2());

Nun können wir diese Threads mittels der Instanzmethode start() starten.

t1.start();
t2.start();

Nachdem Aufruf von start() wird der Code aus der run() Methode in der jeweiligen Runner Klasse aufgerufen.

Wichtigste Erkenntnis hierbei ist, dass der Thread t2 sofort gestartet wird und zwar ohne das gewartet wird bis die Ausführung von t1 beendet ist.

Mit anderen Worten: Die beiden Threads t1 und t2 laufen parallel. Diese Tatsache lässt sich auch anhand der Bildschirmausgabe des Programms beobachten.

Runner1 0
Runner2 0
Runner1 1
Runner2 1
Runner1 2
Runner2 2
Runner1 3
Runner2 3
Runner1 4
Runner2 4
Runner1 5
Runner2 5
Runner1 6
Runner2 6
Runner1 7
Runner1 8
Runner1 9
Runner2 7
Runner1 10
Runner2 8
Runner2 9
Runner2 10

Wie wir sehen ist die Bildschirmausgabe nun nich mehr sequentiell.

Warnung: Möglicherweise ist die Reihenfolge in der Ausgabe bei dir eine andere. Vielleicht verändert sich die Ausgabe sogar von Programmausführung zu Programmausführung. Genau das ist das bereits angesprochene Problem der schweren Reproduzierbarkeit beim Multithreading (Concurrency).

Den Zeitpunkt wann einem Thread bestimmte Hardwareressource zur Verfügung stehen, bestimmt das Betriebssystem und hängt von sich dynamisch veränderten Faktoren ab, wie z.B. von anderen Prozessen, denen ebenfalls Hardwareressourcen zur Verfügung gestellt werden müssen.

Threads mit Hilfe von Vererbung

Eine Alternative zur Verwendung des Runnable Interfaces ist die Verwendung von Vererbung.

So können wir beispielsweise die Klasse Runner1 von der Thread Klasse wie folgt ableiten.

class Runner1 extends Thread{

    public void startRunning() {
      for (int i=0;i<=10;i++) {
        System.out.println("Runner1 "+i);
      }
    }

    @Override
    public void run() {
      startRunning();
    }
}

Genau wie bei dem Vorgehen mit Hilfe des Runnable Interfaces überschreiben wir die run() Methode und platzieren in dieser den Code, welcher nebenläufig ausgeführt werden soll.

Jetzt können wir eine Runner1 Instanz erzeugen und da diese von der Oberklasse Thread erbt, steht hier die start() Methode gleich zur Verfügung.

Wir können den Thread daher auf folgende Weise starten:

Runner1 runner1 = new Runner1();
runner1.start();

Der Weg Threads mit Hilfe von Vererbung zu realisieren führt zwar zu kompakterem Code, hat aber den Nachteil, da JAVA keine Mehrfachvererbung unterstützt, dass Runner1 nicht von einer weiteren Klasse erben kann, weshalb der Weg mittels des Runnable Interfaces flexibler ist.

Außerdem können wir das Runnable Interface nutzen um Threads als innere anonyme Klasse zu definieren.

Warten auf den Tod mittels des Schlüsselwortes join.

Okay, Zeit für ein kleines Quiz. Was denkst du? Wie sieht die Ausgabe folgenden Programms aus?

1: t1.start();
2: t2.start();
3: System.out.println("Thread beendet!");

Oder anders gefragt. Auf welche Ausgabe hoffen wir?

Ganz offensichtlich ist unsere Absicht die Bildschirmausgabe in Zeile drei erst dann auszugeben, wenn die Threads t1 und t2 beendet sind.

Okay, versuchen wir es!

Thread beendet!
Runner1 0
.
.
.
Runner2 10

Ups, das hat wohl nicht funktioniert. Hast du damit gerechnet?

Natürlich! Denn das ist genau der Sinn und Zweck eines Threads. Er soll nebenläufig sein und das Hauptprogramm nicht unterbrechen.

Allerdings gibt es Situationen, wie beispielsweise unsere, in denen das Hauptprogramm (der sogenannte Mainthread), erst weiterlaufen soll, wenn alle Threads abgearbeitet sind.

Und genau für diesen Zweck ist die Methode join() gedacht, welche folgendermaßen verwendet wird:

1: Thread t1 = new Thread(new Runner1());
2: Thread t2 = new Thread(new Runner2());
3: t1.start();
4: t2.start();
5: try {
6:      t1.join();
7:      t2.join();
8: } catch (InterruptedException e) {}
9: System.out.println("Thread beendet!");

Bei der join() Methode handelt es sich um eine Instanz-Methode aus der Thread Klasse.

Die Aufrufe dieser Methode in den Zeilen sechs und sieben sorgen dafür, dass die Bildschirmausgabe in Zeile 9 erst ausgeführt wird, wenn sowohl der Thread t1 als auch t2 gestorben sind.

Da die join Methode eine InterruptedException wirft, müssen wir die Aufrufe in einen entsprechenden try catch Block einbetten.

Wichtig zu verstehen ist, dass die Threads t1 und t2 weiterhin parallel abgearbeitet werden. Diese Tatsache ist auch anhand der Programmausgabe ersichtlich:

Runner2 0
Runner1 0
Runner2 1
Runner1 1
Runner2 2
Runner1 2
.
.
.
.
Runner2 7
Runner1 7
Runner1 8
Runner1 9
Runner1 10
Runner2 8
Runner2 9
Runner2 10
Thread beendet!

Prima, genau was wir wollten!

Das Schlüsselwort volatile

Wir haben bereits festgehalten, dass verschiedene Threads auf dem gleichen Speicher arbeiten.

Doch das ist nur die halbe Wahrheit!

Verschiedene Threads können auf unterschiedlichen CPU’s ablaufen und jede CPU hat einen Zwischenspeicher den sogenannten Cache.

concurrency volatile

Größter Vorteil des Cache ist, dass die CPU auf den Cache weit schneller zugreifen kann als auf den Hauptspeicher des Computers.

Die Caches der unterschiedlichen CPU’s werden allerdings nicht untereinander synchronisiert und das kann dazu führen, dass unsere Threads mit unterschiedlichen Daten arbeiten.

Nehmen wir beispielsweise an wir haben einen booleschen Wert schalter, den wir mit false initialisieren.

Um möglichst effektiv mit diesem Wert arbeiten zu können befindet sich jeweils eine Kopie dieser Variable in jedem Cache unserer CPU’s.

Wird nun im ersten Thread der Wert dieser Variablen von false auf true geändert, passiert das nur in der Kopie dieser Variable, die sich im Cache der ersten CPU befindet.

Im Cache der zweiten CPU enthält die Variable schalter allerdings noch immer den Wert false. Wir haben also eine Inkonsistenz erzeugt.

Und um genau das zu verhindern, gibt es in JAVA das Schlüsselwort volatile.

Eine mit volatile definierte Variable wird nicht im Cache der CPU gepuffert sondern existiert nur EINMAL zentral im Hauptspeicher.

Alle Threads, welche die Variable schalter verwenden, greifen dann auf die gleiche Stelle im Speicher zu. Da es die Variable nur einmal gibt, können nun auch keine Inkonsistenzen mehr auftreten.

Okay, schauen wir uns an, wie wir volatile verwenden müssen.

private volatile boolean schalter = false;

Jap, das ist alles! Wir müssen lediglich bei der Variablen Deklaration volatile vor den Variablennamen schreiben.

Da eine als volatile deklarierte Variable nicht gecached wird, schmälert die Verwendung von volatile geringfügig die Performance unseres Programmes. Dafür gehen wir allerdings dem oben beschriebenen Konsistenz-Problem aus dem Weg.

Was ist ein Deadlock und was ein Livelock?

Bist du ein Handwerker? Hast du einen Werkzeugkoffer?

Nein! Okay, ein Handwerker musst du auch nicht sein. Aber du solltest dir einen Werkzeugkoffer zumindest vorstellen können.

Das wichtigste hierbei ist, dass dein Werkzeugkoffer einen Hammer enthält. Und zwar genau einen!

Nun stelle dir weiter vor, dass du gerade in eine neue Wohnung eingezogen bist und es gibt noch jede Menge Renovierungsarbeiten zu erledigen. Insbesondere müssen noch viele Nägel in die Wand geschlagen werden.

Du bist beliebt und hast einen Haufen Threads! Ähm ich wollte sagen Freunde,  die dir dabei helfen. Aber du hast nur einen Hammer.

Dein allerbester Freund ist Moritz. Er möchte dir wirklich helfen. Aber immer wenn er einen Nagel in die Wand schlagen möchte ist der Hammer von jemand anders in Beschlag und Moritz kann nicht arbeiten.

Er wartet also jedesmal auf eine freiwerdende Ressource (Hammer). Genau das kann auch einem Thread passieren.

Die Situation, dass ein Thread darauf wartet, dass eine Ressource freigegeben wird nennen wir Deadlock.

Neben dem Deadlock gibt es auch noch den sogenannten Livelock.

Im Gegensatz zum Deadlock, bei dem ein Thread im Wartezustand verharrt, ist bei einem Livelock ein Thread dauerhaft beschäftigt und wird niemals Fertig.

Diese Situation tritt ein, wenn dein Paddelboot ein Loch hat, und du den Untergang dadurch vermeidest, dass du das eindringende Wasser mit einem Eimer aus dem Bott herausschöpfst.

Solange du das Boot nicht flickst wirst du mit dieser Aufgabe niemals fertig und du befindest dich in einem Livelock.

Im folgenden wollen wir uns ein Beispiel ansehen, in dem zwei Threads um die gleiche Ressource kämpfen. Hierfür haben wir allerdings zunächst den Begriff atomare Operation zu klären.

Atomare Operationen

Bestimmt kennst du den Begriff Atom aus dem Physik-Unterricht und weißt, dass dieser Begriff ein nicht weiter zerlegbares Elementarteilchen beschreibt.

Und ganz ähnlich ist der Begriff atomar auch in der Informatik zu interpretieren.

Wir sprechen von einer atomaren Operation, wenn diese in einem Schritt verarbeitet werden kann. Beispiele für atomare Operationen sind Lese- und Schreibzugriffe.

Aber bereits ein einfaches Inkrement i++ ist nicht atomar (siehe auch). Sondern wird intern in folgende atomare Operation gesplittet.

  1. Die Variable i wird auf den Ausführungs-Stack gelegt.
  2. Der Wert 1 wird auf den Ausführungs-Stack gelegt.
  3. Der Wert i und 1 wird addiert.
  4. Das Ergebnis wird in den Speicher geschrieben.

Was hat das mit Multithreading zu tun?

Nun ja, was passiert, wenn zwei verschiedene Threads die Variable i inkrementieren wollen?

Dann müssen wir höllisch aufpassen, dass sich die Threads während der Abarbeitung der atomaren Operationen nicht dazwischen Funken.

Nehmen wir beispielsweise an Thread 2 holt sich den Wert der Variable i nachdem diese zwar bereits durch Thread 1 erhöht wurde aber noch nicht zurückgeschrieben ist.

In diesem Fall arbeitet Thread mit einem um 1 zu kleinen Wert von i.

Um das zu verhindern gibt es das Schlüsselwort synchronize.

Mit synchronize können sowohl Methoden als auch Codeblöcke deklariert werden. Wir beschränken uns allerdings auf die Verwendung von synchronize auf Methodenebene.

Eine Methode welche ein Thread sicheres Inkrement durchführt sieht folgendermaßen aus.

1: private static synchronized void increment() {
2:    ++count;
3: }

Der Rumpf dieser Methode kann nur durch einen einzigen Thread gleichzeitig, also insbesondere nicht parallel, ausgeführt werden.

Da das ein wenig den Sinn von Threads untergräbt, sollten wir synchronize vorsichtig einsetzen. Ansonsten verlieren wir die durch den Einsatz von Multithreading gewonnene Performance schnell wieder.

Tritt öfter die Situation ein, dass Threads zum stehen kommen, weil eine Ressource belegt ist, so sollten wir das Design unserer Multithreadding Anwendung überprüfen und gegebenenfalls ändern.

Die Schlüsselwörter wait und notify!

Okay, das war bis hier bereits eine Menge Stoff, der auch erstmal verdaut werden will.

Aber eines müssen wir uns noch anschauen. Nämlich die Funktion der Schlüsselwörter wait und notify.

Diese Schlüsselwörter sind Grundlage des wichtigen Producer Consumer Patterns.

Bei diesem Pattern sind mindestens zwei Threads beteiligt, welche miteinander kommunizieren.

Ein (mindestens) Thread ist der sogenannte Producer und (mindestens) ein anderer Thread der Consumer.

Hier ist der Name Programm! Der Producer ist dafür verantwortlich Daten zu produzieren, die der Consumer verarbeitet.

In der Regel werden die Ergebnisse des Producers zunächst in einer Queue zwischengespeichert.

Leider sprengt eine detaillierte Betrachtung des Consumer Producers Patterns den Rahmen diese Artikels.

Wichtig für uns ist hier nur, dass der Producer und der Consumer miteinander kommunizieren müssen.

Der Consumer muss solange warten bis der Producer genügend Daten produziert hat, die verarbeitet werden können.

Sobald genügend Daten produziert wurden, muss der Consumer benachrichtigt werden, dass dieser nun mit seiner Arbeit beginnen kann.

Damit der Consumer nicht überfordert wird soll der Producer erst wieder arbeiten sobald der Consumer die Daten in der Queue vollständig verarbeitet hat.

Der Producer muss also in den Wartezustand versetzt werden wofür das Schlüsselwort wait() existiert.

Um die Benachrichtigung zwischen den Threads zu realisieren verwenden wir das Schlüsselwort notify().

Mit notify() heben wir also den Wartezustand eines Threads wieder auf.

Okay, schauen wir uns das ganze mal in Aktion an.

Im ersten ist die Queue leer um diese zu füllen arbeitet der Producer-Thread bis die gefüllt ist.

Concurreny Producer Consumer

Sobald die Queue gefüllt ist wird der Consumer mit Hilfe von notify() darüber in Kenntnis gesetzt und fängt an die Queue zu abzuarbeiten. Der Producer nimmt in dieser Zeit den Status wait an.

Consumer_Producer_Pattern

Ist die Queue leer, erhält wiederum der Producer ein notify und beginnt zu arbeiten. Der Consumer fällt zurück in den wait Modus.

Consumer_Producer_Pattern

Fazit: Ziel dieses Artikels war es dir einen guten Überblick über das Thema Concurrency und parallele Programmierung zu geben. Insbesondere haben wir uns angesehen wie Threads in Java funktionieren.

Hat dir der Artikel gefallen? Dann folge uns doch gleich auf Facebook.

Hallo ich bin Kim und ich möchte ein großer Programmierer werden. Machst du mit?

Kommentare (4)

  • Antworte

    Ein Lob auf deine Seite. Hier werden ein Haufen Seiten aus der Fachliteratur einfach und verständlich zusammengefasst. Eine tolle Ergänzung zum lernen des Themas.

    • Hallo Andreas, vielen Dank für deine Rückmeldung! Viele Grüße Kim

  • Antworte

    Wow, so toll erklärt 🙂

    Ich mochte deinen Erzählstil sowie die Absätze dazwischen, das sieht dann nicht so vollgepackt aus.

    Vielen lieben Dank!

    • Ich danke dir! Viele Grüße Kim

Hinterlasse ein Kommentar