Wie im vorherigen Kapitel angesprochen muss eine Netzwerkverbindung zwischen einem Server und einem Client bestehen, damit diese miteinander kommunizieren können. Es gibt mittlerweile einige Programmierschnittstellen, die den Zugriff auf TCP/IP durch ein Java Programm erlauben. Somit kann ein Programm Verbindung mit einem Server aufnehmen und Daten mit diesem austauschen. Dies erfolgt in Java über Klassen in der java.net Bibliothek. In den meisten Fällen geschieht die Datenübertragung zwischen Server und Host auf der Basis von TCP. Die java.net Socket Klasse stellt eine solche TCP-Verbindung zur Verfügung.
Echtzeit-Chat
Das Computerspiel, welches im vorherigen Kapitel definiert worden ist, soll nun um einen Echtzeit-Chat erweitert werden. Hier haben die Spieler dann die Möglichkeit sich gegenseitig über den Hausbau auszutauschen und können zusammenarbeiten.
Voraussetzung für Echtzeit-Chatprogramme ist, dass der Server alle Clients, und die Clients jeweils den Server kennen. Dabei ist nicht wichtig, dass sich die Clients untereinander kennen. Im ersten Schritt verbindet sich ein Client mit dem Server und stellt eine Verbindungsanfrage mit dem Chat- Dienst. Danach stellt der Server eine Verbindung her und sendet die Bestätigung als Antwort an den Client zurück. Dieser Vorgang erfolgt für alle weiteren Teilnehmern, die dem Chat beitreten wollen. Sendet nun ein Client eine Nachricht an den Chat, geht die Nachricht zunächst beim Server ein und dieser verteilt die Nachricht an alle Teilnehmer des Chats.
Um nun zwischen Client und Server kommunizieren zu können, muss zunächst eine neue Socket- Verbindung vom Client zum Port des Servers hergestellt werden. Dies erfolgt durch ein Socket- Objekt (Klasse: java.net.Socket):
Socket chatSocket = new Socket (“127.0.0.1“, 5000);
Das Objekt bildet eine Beziehung zwischen zwei Maschinen, die voneinander wissen. Hierfür braucht man zwei Informationen: die IP-Adresse (z.B. 127.0.0.1) und die TCP-Portnummer (z.B. 5000) der jeweiligen Maschinen. Die TCP-Portnummer ist eine 16-Bit-Zahl zur Identifizierung eines bestimmten Programms auf dem Server und gelten somit als unverwechselbare Identifikationsmerkmale. Der Server weiß somit genau, mit welcher Anwendung sich ein Client verbinden möchte.
Folgend wird dann ein InputStreamReader erzeugt, der eine Verbindung zwischen dem Bytestrom des Sockets und dem Zeichenstrom des Readers herstellen kann:
InputStreamReader strom = new InputStreamReader (chatSocket.getInputStream());
Anschließend wird noch ein BufferedReader implementiert, der mit dem InputStreamReader von eben verkettet ist:
BufferedReader reader = new BufferedReader (strom);
String message = reader.readLines();
So besteht nun eine Verbindung, die das Lesen von Daten vom Server ermöglicht. Der Server sendet Bytes zum InputStreamReader, dieser verwandelt die Bytes in Zeichen und gibt sie an den BufferedReader weiter, der schlussendlich die Zeichen beim Client ausgeben kann.
Sollen nicht Daten ausgelesen, sondern geschrieben werden, muss zunächst wieder eine Verbindung zwischen Client und Server hergestellt werden. Dies funktioniert auf dieselbe Art und Weise wie beim Lesen von Daten (siehe oben). Ist das Socket-Objekt erzeugt, wird nun der PrintWriter eingesetzt, um Daten in Zeichenform in Bytes umzuwandeln und in den Strom zum Server zu schicken:
PrintWriter writer = new PrintWriter (chatSocket.getOutputStream());
Somit sind nun alle nötigen Verbindungen hergestellt und der entsprechende Text kann ausgegeben und gelesen werden.
Implementieren eines Servers
Client- und Serververbindungen kommunizieren über eine Socket-Verbindung. Für die Implementierung eines Servers benötigt man einen ServerSocket, der auf eine Client-Anfrage wartet. Zudem wird ebenso ein einfacher Socket benötigt, der für die Kommunikation mit dem Client benutzt wird.
Im ersten Schritt muss ein neues ServerSocket-Objekt erstellt werden:
ServerSocket serverSock = new ServerSocket (4242);
Damit hat die Server Anwendung nun eine „Anlaufstelle“ für eingehende Client-Anfragen auf dem Port 4242.
Im zweiten Schritt erstellt der Client nun eine Socket-Verbindung zu dem ServerSocket, indem er ein neues Socket-Objekt mit der gewünschten IP-Adresse und Portnummer erstellt.
Im letzten Schritt wird auf dem Server dann ein neues Socket für die Kommunikation mit jedem einzelnen Client erstellt. Wenn bei einem ServerSocket eine Anfrage eingeht „akzeptiert“ dieser die Anfrage, indem er eine Socket-Verbindung zu dem Client herstellt:
Socket sock = serverSock.accept();
Multithreading in Java
Multithreading ist die nebenläufige bzw. parallele Ausführung mehrerer Handlungsstränge, die auf die gleichen Daten zugreifen, wobei jede Ausführung eine andere Aufgabe erledigt. Das Programm muss sich einen Thread als auch seine ausführende Aufgabe ansehen. Ein Ausführungsstrang wird zunächst durch die Erzeugung eines neuen Thread-Objektes implementiert:
Thread t = new Thread ();
t. start();
Ein Thread wird in Java von einem Objekt der Klasse Thread repräsentiert. Ein Thread durchläuft in seinem Lebenszyklus verschiedene Phasen. Er startet, läuft und stirbt. Die Klasse Thread enthält beispielsweise folgende Methoden:
- Starten eines neuen Threads: void start();
- Verbindung mit einem anderen Thread: void join();
Nur ein echtes Multiprozessorsystem erlaubt die gleichzeitige Ausführung eines Programms. Java-Threads werden in Wirklichkeit nicht exakt gleich ausgeführt, sondern wechseln sehr schnell zwischen den Stacks hin- und her, sodass der Eindruck erweckt wird, dass alle Stacks zur gleichen Zeit ausgeführt werden. Jeder Thread hat in Java seinen eigenen Aufruf-Stack. Um genau zu definieren, welche Arbeit der Threads erledigt werden soll, wird das Runnable-Objekt implementiert (in einer Klasse festgelegt):
Runnable threadJob = new MeinRunnable();
Nun wird dem Thread-Konstruktor das neue Runnable-Objekt übergeben. Das Thread-Objekt erfährt nun welche Methode er auf seinen neuen Stack setzten soll – die run() Methode des Runnable-Objektes:
Thread meinThread = new Thread (threadJob);
Als letzten Schritt muss die start() Methode des Threads aufgerufen werden:
meinThread.start();
Die bloße Thread-Instanz wandelt sich nun in einen Ausführungs-Thread um. Wenn der neue Thread startet, nimmt er die run() Methode des Runnable-Objektes und setzt sie zuunterst auf seinem Stack. Es ist somit die erste Methode, die in dem neuen Thread ausgeführt wird.
Das Interface Runnable definiert nur eine einzige methode, public void run(). Um einen Job für den Thread zu erzeugen, muss das Interface Runnable entsprechend implementiert werden.
Thread Zustände
Durch die Zeile Thread t = new Thread(r); wird, wie oben erläutert, eine neue Thread-Instanz erzeugt. Dieser Thread wird jedoch noch nicht ausgeführt.
t.start(); startet den Thread. Er wird somit lauffähig. Er ist in der „Startposition“ und wartet drauf zur Ausführung ausgewählt zu werden.
Nachdem er lauffähig ist, wird er zur Ausführung ausgewählt und ist somit der laufende Thread. In diesem Zustand besitzt der Thread einen aktiven Aufruf-Stack und die oberste Methode auf dem Stack wird ausgeführt.
Nachdem ein Thread lauffähig ist, kann er zwischen den Zuständen lauffähig, laufend und vorübergehen nicht lauffähig hin und her wechseln. Typischerweise wechselt ein Thread zwischen lauffähigen und laufenden Zustand hin und her. Nachdem der Thread von der JVM zur Ausführung ausgewählt worden ist, gelangt er in den Zustand lauffähig zurück und ein anderer Thread wird laufend. Ebenfalls kann der Thread in einen vorübergehend blockierten Zustand versetzt werden. Beispielsweise ist dies der Fall, wenn der ausführende Code den Thread angewiesen hat, sich schlafen zu legen (sleep()).

Der Thread-Scheduler
In Java ist der Thread Scheduler Teil der JVM (Java Virtual Machine) und auf verschiedenen JVMs unterschiedlich implementiert. Für die Planung und die Entscheidung der Thread-Zustände ist der Thread-Scheduler zuständig. Er bestimmt, welcher Thread wie lange läuft und was aus den Threads wird, wenn der Thread-Scheduler sie aus dem laufenden Zustand herausnimmt. Es ist nicht vollkommen vorhersehbar, welche Entscheidung der Scheduler trifft, d. h. welcher Thread als nächstes für wie lange in den laufenden Zustand versetzt wird. Es ist somit nicht davon auszugehen, dass alle Threads fair und gleichberechtigt nacheinander an die Reihe kommen.
Schlafen legen eines Threads
Die statische Methode ermöglicht es, Threads trotz des unvorhersehbaren Verhaltens des Thread-Schedulers abwechselnd ihre Programme ausführen zu lassen.
Thread.sleep (2000);
Die Methode versetzt den Thread von seinem laufenden Zustand in einen inaktiven Zustand, bevor der Thread wieder in den Zustand lauffähig zurückkehren kann. Dann wartete der Thread darauf, dass der Thread-Scheduler ihn wieder in den laufenden Zustand versetzt. Bei Aufruf der Methode wird die Dauer des „Schlafes“ in Millisekunden festgelegt (in unserem Beispiel 2.000 Millisekunden). Allerdings löst die Methode eine Interrupted Exception aus; daher ist die Verwendung von try-catch Blöcken obligatorisch.
try {
Thread.sleep(2000);
} catch (InterruptesException ex) {
ex.printStackTrace();
}
Nebenläufigkeitsprobleme
Nebenläufigkeit bezeichnet auch das parallele Ablaufen von Anweisungen oder Befehlen in der Informatik. Dabei kann es zu Dateninkonsistenzen kommen, wenn gleichzeitig unterschiedliche Threads auf die Daten desselben Objektes zugreifen wollen. Der eine Thread führt seine Aufgabe aus, während der andere Thread lauffähig (oder blockiert) ist. Nachdem er wieder in den laufenden Zustand zurückkehrt, weiß dieser gar nicht, dass er jemals angehalten hat.
Man kann die Nebenläufigkeitsprobleme anhand des folgenden Beispiels veranschaulichen:
Rainer und Monika sind verheiratet und teilen sich ein Bankkonto. Sie haben die Vereinbarung, dass keiner von beiden das Konto überziehen darf. Das heißt, beide müssen den Kontostand vor jeder Abhebung überprüfen und dadurch sicherstellen, dass das Konto nicht überzogen wird.
Rainer möchte 50 Euro abheben, checkt den Kontostand auf dem sich aktuell 100 Euro befinden. Somit steht seiner Abhebung nichts im Wege. Bevor er das Geld abgehoben hat, schläft er jedoch ein. Während Rainer schläft tätigt Monika eine Abhebung von 100 Euro. Nachdem die 100 Euro von Monika abgehoben worden sind und das Konto somit leergeräumt ist, wacht Rainer wieder auf und möchte die geplanten 50 Euro abheben. Nachdem Rainer vor seinem Schlaf den Kontostand nachgeschaut hat und nun davon ausgeht, dass dieser noch 100 Euro beträgt, hebt er das Geld ab und überzieht nichtwissend das Bankkonto.
In Codeform haben wir ein Objekt, das Bankkonto, dass sich zwei Threads, Rainer und Monika, miteinander teilen. Mit der Methode setName() ist es möglich Threads zu benennen. Wir haben zwei Klassen, Bankkonto und RainerUndMonikaJob. Die Klasse Bankkonto zeigt den Kontostand an und führt die Abhebung durch. Die Klasse RainerUndMonikaJob implementiert Runnable und repräsentiert das Verhalten: Kontostand überprüfen und Abhebung durchführen, sowie das einschlafen. Die Klasse RainerUndMonikaJob hat eine Instanzvariable vom Typ Bankkonto, die das gemeinsame Konto repräsentiert.
Synchronisierung
Wie können wir nun sicherstellen, dass die Threads nicht zu Nebenläufigkeitsproblemen führen? Die Antwort ist ein Schloss. Im vorliegenden Beispiel benötigen wir ein Schloss für die Transaktion Kontostand prüfen und Geld abheben. Die Transaktion ist unversperrt, wenn gerade niemand auf das Konto zugreift. Will jedoch einer der beiden auf das Konto zugreifen wird das Schloss so lange gesperrt, bis die Transaktion des Geldabhebens durchgeführt ist. Die Folgerung daraus ist, dass der jeweils andere in der Zwischenzeit nicht auf das Konto zugreifen kann.
In Bezug auf den Code müssen wir dafür sorgen, dass ein Thread, sobald die entsprechende Methode eingetreten ist, diese Methode zu Ende ausgeführt werden muss, bevor irgendein anderer Thread hineindarf. Dies können wir durch das Schlüsselwort synchronized garantieren. Die Methode wird so modifiziert, dass immer nur ein Thread auf einmal auf diese zugreifen kann.
Jedes Objekt hat ein Schloss. Das Schloss ist meist offen, denn Objektsperren kommen nur dann ins Spiel, wenn es synchronisierte Methoden gibt. Das Sperren gehört nicht nur einer Methode, sondern zum Objekt.
Thread-Kommunikation
Die Methode public final void join() zwingt den gerade ausgeführten Thread mit seiner weiteren Ausführung so lange zu warten, bis der Thread, für den join ausgeführt wird, beendet ist. Ebenfalls kann ich in die Methodenklammer die Millisekunden initialisieren (long millis), die das Thread warten muss.
Die Methode public final void notify() reaktiviert einen einzelnen Thread, der sich im Wartezustand bezüglich des aktuellen Objektes befindet. Anstelle von notify kann ebenso notifyAll verwendet werden und reaktiviert alle „wartenden Threads“.
Die Methode public final void wait() hingegen veranlasst den Thread, der gerade ausgeführt wird, mit seiner Ausführung zu warten, bis ein anderer Thread die notify- oder die notifyAll-Methode für das aktuelle Objekt ausführt. Der Thread gibt dazu die Objekt-Sperre ab und muss sie nach dem Wartevorgang wieder erwerben. Ebenfalls kann ich durch long timeout die Millisekunden des Wartens initialisieren.
Das Schlüsselwort synchronized ist in Verbindung mit den Methoden join, wait, notify und notifyAll ein zuverlässiger Mechanismus für den Schutz kritischer Programmbereiche.
Hier gehts weiter zum Themenbereich PHP.