Die Kooperationspartner in einem verteilten System, das sind Threads, müssen miteinander kommunizieren, um gemeinsam an einer Aufgabe arbeiten zu können. Die Techniken, die für eine solche Inter-Thread-Kommunikation eingesetzt werden, hängen in einem hohen Maß davon ab, in welcher Beziehung sich die miteinander kommunizierenden Threads zueinander befinden. Dabei gibt es drei Situationen, die ganz unterschiedliche Anforderungen an die Bereitstellung der jeweils notwendigen technischen Hilfsmittel stellen. Eine Inter-Thread-Kommunikation ist entweder
Eine Inter-Thread-Kommunikation heißt prozesslokal, wenn die beiden miteinander kommunizierenden Threads zu ein und demselben Prozess gehören. In diesem Fall können die Threads die Data Region ihres Prozesses als gemeinsamen Speicher verwenden. Die Hilfsmittel, die von der Sprache Java und ihrem Laufzeitsystem (der JVM) zur Verfügung gestellt werden, reichen dafür aus.
Dass die Data Region eines Prozesses für alle seine Threads ein gemeinsamer Speicher ist, hat im Abschnitt 1.3.3 (Threadsicherheit) dazu geführt, dass bei einem nebenläufigen Zugriff auf diesen Speicher Schreibkonflikte aufgetreten sind. In den beiden Programmen
greifen zwei Threads auf eine gemeinsame Variable zu. Über solche Variablen können sie miteinander kommunizieren. Allerdings zeigen die beiden Programmbeispiele, dass ein Zugriff auf diesen gemeinsamen Speicher sorgfältig synchronisiert werden muss, was eine Aufgabe der Anwendungsprogrammierung ist. Um die Programmierung einer prozesslokalen Inter-Thread-Kommunikation zu erleichtern, stellt Java eine Pipeline-Technik zur Verfügung. Mit den Klassen
aus dem java.io-Package und ihren Methoden wird anschaulich eine gerichtete Röhre (eine Pipeline) zwischen zwei prozesslokalen Threads eingerichtet und betrieben.
Für das Schreiben in und das Lesen aus einer solchen Röhre wurden die aus dem Arbeiten mit Dateien bekannten Methoden übertragen. Einer der beiden miteinander kommunizierenden Threads schreibt in die Pipeline, so als wäre sie eine Datei, der andere liest aus der Pipeline, so als lese er aus einer Datei. Die Implementierung der Pipeline
Das sind alles Aufgaben, die bei einer Nutzung der Data Region als gemeinsamer Speicher von der Anwendungsprogrammierung übernommen werden müssen. Das folgende Beispiel zeigt den Gebrauch einer Java-Pipeline:
Der main-Thread erzeugt je eine Instanz der Klassen PipedReader und PipedWriter und verbindet sie miteinander zu einer Pipeline mit einer Schreib- und einer Leseseite. Anschließend startet er zwei Threads, von denen einer, hier Schreiber genannt, in die Pipeline schreibt, der andere (Leser) aus ihr liest. Man beachte in der run()-Methode des Schreibers die beiden Befehle nach dem Schreiben in die Pipeline:
Eine prozessübergreifende, rechnerlokale Inter-Thread-Kommunikation liegt vor, wenn zwei Threads, die miteinander kommunizieren sollen, zu zwei Prozessen gehören, die beide auf ein und demselben Rechner laufen. Um in einer solchen Situation eine Kommunikation zwischen den beiden Threads zu ermöglichen, muss das Betriebssystem des Rechners einbezogen werden, denn es isoliert die Adressräume der Prozesse voneinander und schützt sie so.
Das Dateisystem eines Rechners ist Teil des Betriebssystems und steht jedem Prozess (und damit dessen Threads) auf diesem Rechner zur Verfügung. Für alle Prozesse des Rechners ist das Dateisystem ein gemeinsamer Speicher. Allerdings stellt das Betriebssystem keinerlei Synchronisationshilfen für eine direkte Benutzung dieses Speichers zur Verfügung.
Als Alternative zur direkten Benutzung des Dateisystems für eine Kommunikation zwischen Prozessen (für eine Inter-Prozess-Kommunikation) sind im Lauf der Jahre bei den Betriebssystemen eine ganze Reihe von Kommunikationshilfen für Prozesse entwickelt worden. Dazu gehören typischerweise
Bei dieser Technik richtet das Betriebssystem im Hauptspeicher des Rechners einen Bereich ein, der beiden Prozessen als zusätzliche Region ihres jeweiligen Adressraums zur Verfügung gestellt wird. Dieser Bereich wird als Shared Memory Region bezeichnet. Für jede der beiden beteiligten Prozesse ist diese Region Teil ihres eigenen Adressraums. Auch bei dieser Technik bleibt die Synchronisation der Zugriffe auf das Shared Memory der Anwendungsprogrammierung überlassen.
Die Pipeline-Technik bei Betriebssystemen ist konzeptionell identisch mit der, die bereits als Java-Verfahren für eine prozesslokale Inter-Thread-Kommunikation vorgestellt worden ist. Viele Betriebssysteme stellen ihren Prozessen und deren Threads eine solche Kommunikationshilfe zur Verfügung. Unter der Kontrolle des Betriebssystems schreibt der eine Thread in die Pipeline und der andere liest daraus.
In die meisten Kommandointerpreter, wie beispielsweise in die UNIX- und Linux-Shells und in die Command.com, Cmd.exe und PowerShell von Microsoft, sind Pipeline-Techniken integriert. Ihre Funktionalität ist dabei dahingehend eingeschränkt, dass sie nur in Richtung der Kommandoschreibweise, also anschaulich von links nach rechts, Daten übertragen können. Üblicherweise wird als Symbol für eine Betriebssystem-Pipeline in den Kommandointerpretern der senkrechte Strich ( | ) verwendet. In dem folgenden Beispiel aus der UNIX-Bourne-Shell
wird die Ausgabe des who-Kommandos in eine Pipeline geschrieben, aus der das sort-Kommando liest und das Ergebnis auf der Konsole ausgibt.
Es gibt sehr einfache Kommunikationssituationen, die mit Hilfe einer Shared-Memory- oder Pipeline-Technik allein nicht lösbar sind. Das folgende Beispiel zeigt eine solche Situation:
Will in diesem Beispiel der Thread t1 an den Thread t5 das Zeichen 'a' schicken, dann ist ohne zusätzliche Maßnahmen nicht verhinderbar, dass dieses Zeichen von t3 oder von t4 aus dem Gemeinsamen Speicher oder aus der Pipeline gelesen und daraus entfernt wird.
Um derartige Probleme zu lösen, stellen viele Betriebssysteme für die Inter-Prozess-Kommunikation ein asynchron arbeitendes Message-Passing-Verfahren zur Verfügung. Dabei erzeugt der eine Prozess Nachrichten, das können beliebige Datenstrukturen sein, und übergibt sie dem Betriebssystem. Dieses verwaltet die Nachrichten und stellt sie anfragenden Prozessen nach einer Berechtigungsprüfung zur Verfügung.
Zwei Threads ein und desselben Prozesses können von der Programmierung immer so betrachtet werden, als seien sie Threads zweier verschiedener Prozesse. Können Threads die Dienste des Betriebssystems, wie zum Beispiel eine Pipelinetechnik, nutzen, dann können sie dies auch dann tun, wenn sie sich in einer prozesslokalen Situation befinden. Allerdings ist es ressourcenschonender, Betriebssystemdienste nur dann in Anspruch zu nehmen, wenn es erforderlich ist.
Betriebssysteme stellen den Anwendungen ihre Dienste über Systemaufrufe zur Verfügung. Das heißt, dass ein Programm, das Betriebssystemdienste in Anspruch nehmen will, dies ausschließlich über entsprechende Systemaufrufe erreichen kann. Das Java-System kann auf das jeweilige Dateisystem des Rechners unter anderem über die InputStream-, OutputStream-, Reader- und Writer-Klassen zugreifen, enthält jedoch keine allgemeine Systemaufruf-Schnittstelle. Stellt das Betriebssystem beispielsweise ein Message Passing zur Verfügung, dann kann ein auf der Java-Standard-Klassenbibliothek beruhendes Programm diesen Dienst nicht nutzen. Allerdings gibt es spezielle (kommerzielle) Java-Bibliotheken mit geeigneten Methoden. Alternativ kann mit dem Java Native Interface (JNI) gearbeitet werden, mit dem unter anderem C- oder C++-Programme in ein Java-Programm eingebunden werden können, die ihrerseits Schnittstellen zu den Systemaufrufen anbieten.
Gehören zwei Threads, die miteinander kommunizieren sollen, zu zwei Prozessen und jeder von diesen Prozessen zu jeweils einem anderen Rechner, dann sind die beiden Threads in einer rechnerübergreifenden Situation. Um in diesem Fall eine Kommunikation zwischen den beiden Threads zu ermöglichen, reichen die Hilfsmittel der beiden zugehörigen Betriebssysteme nicht mehr aus. Jetzt ist es erforderlich, für den Datentransport ein Netzwerksystem zu verwenden.
Im Internet sind für die Netzwerkkommunikation die bereits erwähnten TCP/IP-Protokolle (siehe dazu die Kapitel 3, 4 und 5 dieses Manuskripts) entwickelt worden. Sie stellen für die Programmierung sogenannte Sockets zur Verfügung, die im nächsten Kapitel (2 Programmierung) vertieft behandelt werden. Hier soll eine grobe Beschreibung genügen.
Sockets (auf Deutsch: Sockel) gibt es in zwei Ausprägungen: In der einen wird zwischen den beiden Kommunikationspartnern eine Art Pipeline realisiert. Diese Ausprägung wird als Stream Socket bezeichnet. Bei der anderen Ausprägung, die Datagram Socket heißt, werden voneinander unabhängige Datenpakete, die man Datagramme nennt, ausgetauscht. Java stellt in seiner Standardbibliothek Klassen für das Arbeiten mit beiden Socketausprägungen zur Verfügung.
Sollte ein Netzwerksystem im Einsatz sein, dann können Threads auch in einer rechnerlokalen und sogar in einer prozesslokalen Situation die Kommunikationsmittel des Netzwerksystems nutzen. Allerdings ist auch hier ein Anmahnen der Verhältnismäßigkeit der Ressourcennutzung angebracht.