In den meisten Programmiersprachen arbeitet man mit einzelnen sequentiellen Ausführungssträngen ( single threaded).
Realistischer ist oft die Modellierung durch parallele Ausführungsstränge ( multi threaded).
Arbeiten parallele Ausführungsstränge auf gemeinsamen Daten, entstehen
zufällige Ergebnisse ( race hazards) , wenn
die Threads nicht bezüglich dieser Daten synchronisiert werden.
Java bietet die Mechanismen zur Programmierung paralleler Ausführungsstränge:
Die Klasse Thread steht in der Bibliothek java.lang zur Verfügung.
Es gibt zwei Arten, Threads zu definieren und zu starten:
1. Möglichkeit
Definiere eine Unterklasse von Thread.
Überschreibe deren Methode run.
Dann werden Objekte dieser Unterklasse erzeugt und gestartet
(Methode start).
Beispiel:
class PrimThread extends Thread { long minPrim; PrimThread(long minPrim) { this.minPrim = minPrim; } public void run() { // Berechne Primzahlen // größer als minPrim . . . } }
Starten durch:
PrimThread p = new PrimThread(143); p.start();
2. Möglichkeit
Definiere ein Klasse, die das Interface Runnable
implementiert.
Erzeuge ein Objekt dieser Klasse, übergebe es als
Argument der Thread-Erzeugung und starte den
erzeugten Thread.
Beispiel:
class PrimRun implements Runnable { long minPrim; PrimRun(long minPrim) { this.minPrim = minPrim; } public void run() { // Berechne Primzahlen // größer als minPrim . . . } }
Starten durch:
PrimRun p = new PrimRun(143); new Thread(p).start();
Beispiel:
class Uthread extends Thread { public Uthread(String str) { super(str); // Thread hat einen Namen } public void run() { for (int i = 0; i < 5; i++) { System.out.println(i+" "+getName()); try { sleep((int)(Math.random()*1000)); } catch (InterruptedException e) {} } System.out.println("1998: "+getName()); } } class Urlaub { public static void main (String[] args) { new Uthread("Sauerland").start(); new Uthread("Malediven").start(); } }
Könnte z.B. folgende Ausgabe erzeugen:
0 Sauerland 0 Malediven 1 Sauerland 2 Sauerland 3 Sauerland 1 Malediven 4 Sauerland 1998: Sauerland 2 Malediven 3 Malediven 4 Malediven 1998: Malediven
Wichtiges Anwendungsfeld für Threads sind auch Applets. Sie brauchen
Threads, um die Anzeige regelmäßig auffrischen zu können ohne andere
Abläufe zu behindern.
Beispiel: Digitaluhr
import java.awt.Graphics; import java.util.Date; public class Clock extends java.applet.Applet implements Runnable { Thread digital; public void start() { if (digital == null) { digital = new Thread(this, "Clock"); digital.start(); } } public void run() { while (digital != null) { repaint(); try { Thread.sleep(1000);} catch (InterruptedException e){} } } public void paint(Graphics g) { g.drawString (new Date().toString(), 5, 10); } public void stop() { digital = null; } }
Für den Übergang zwischen Runnable und Not Runnable
gibt es vier Gründe:
Synchronisation von Threads dient der Sicherstellung des gegenseitigen Ausschlusses beim Zugriff auf gemeinsam benutzte Daten.
Java bietet zwei Konstrukte
Wird eine als synchronized markierte Methode für ein Objekt aufgerufen, so gilt dieses Objekt als gesperrt ( locked).
Alle anderen Threads, die ebenfalls eine synchronisierte Methode für dieses Objekt aufrufen, werden blockiert, bis diese Sperre aufgehoben ist.
So läßt sich gegenseitiger Ausschluß beim Zugriff auf Daten sicherstellen.
Die Java-Synchronisation beruht auf Monitoren (Hoare, 1974)
(siehe Seite ). Es gibt allerdings keine
expliziten lock- und unlock-Operationen.
Beispiel:
class Konto { private double Kontostand; public Konto(double anf) { Kontostand = anf; } public synchronized double getKontostand() { return Kontostand; } public synchronized void setKontostand(double d) { Kontostand = d; } public synchronized void einzahlen(double b) { double a = getKontostand(); a += b; setKontostand(a); } }
Synchronisierte Klassenmethoden
synchronized-Markierung geht auch bei Klassenmethoden :
Überschreiben von synchronisierten Methoden
Eine Methode, die eine synchronisierte Methode überschreibt, kann entweder synchronisiert oder nicht synchronisiert sein.
Die Eigenschaft der überschriebenen Methode, Objekte zu sperren, geht
durch die nicht synchronisierte überschreibende Methode nicht verloren.
class Ober { synchonized void method() { ... } ... } class Unter extends Ober { void method() { super.method(); // jetzt sperren ... } ... }
Synchronisierte Anweisungen dienen dazu, bezüglich Objekten zu synchronisieren auch wenn keine synchronisierten Methoden vorhanden sind.
Form:
synchronized(expr) statement
Bedeutung:
Wenn der Thread die Sperre für das Objekt expr erhält, kann die Anweisung statement ausgeführt werden.
Beispiel:
// numbs soll nach der folgenden // Schleife Absolutwerte enthalten synchronized (numbs) { for (int i = 0; i < arr.length; i++) if (numbs[i] < 0) numbs[i] = -numbs[i]; }
Die Sperren, die zur Realisierung
synchronisierter Anweisungen verwendet werden,
sind die gleichen wie für synchronisierte Methoden.
Die beiden Synchronisations-Schreibweisen von Java können also
zusammenarbeiten.
Aktiv zusammenarbeitende Threads müssen zusätzlich über
Eigenschaften gemeinsam benutzter Ressourcen
informiert sein.
Typisches Beispiel: Produzent/Konsument-Kommunikation
class Produzent extends Thread { private Puffer puffer; public Produzent(Puffer p) { puffer = p; } public void run() { for (int i = 0; i < 5; i++) { puffer.put(i); System.out.println ("Produzent gibt: " + i); try { sleep((int)(Math.random() * 1000)); } catch (InterruptedException e) {} } } }
class Konsument extends Thread { private Puffer puffer; public Konsument(Puffer p) { puffer = p; } public void run() { int wert = 0; for (int i = 0; i < 5; i++) { wert = puffer.get(); System.out.println ("Konsument nimmt " + wert); } } }
Das Hauptprogramm, das Produzent und Konsument startet:
class P_K_Test { public static void main(String[] args) { Puffer p = new Puffer(); new Produzent(p).start(); new Konsument(p).start(); } }
Annahme: Der benutzte Puffer kann genau eine Zahl aufnehmen:
Wenn Produzent und Konsument sich nicht über Eigenschaften der
gemeinsam benutzten Ressource vom Typ Puffer verständigen,
wird das Ergebnis falsch:
Die Synchronisation zwischen Produzent und Konsument geschieht
in Java über Monitore.
Ein Thread im Monitor kann wait() aufrufen, um den Monitor freizugeben, während er auf eine bestimmte Bedingung wartet.
Die notify()- und notifyAll()-Methoden
Ein Thread im Monitor kann notifyAll() oder notify() aufrufen, um andere Threads, die wegen der gleichen Bedingungsvariable warten, wieder ausführbereit zu machen.
notifyAll macht alle wartenden Threads ausführbereit.
notify wählt aus den wartenden Threads einen aus, der
ausführbereit wird.
Der Puffer für das Produzent/Konsument-Schema
class Puffer { // Pufferelement zwischen Produzent // und Konsument private int inhalt; private boolean gefüllt = false; public synchronized int get() { while (gefüllt == false) { try {wait();} catch (InterruptedException e) {} } gefüllt = false; notifyAll(); return inhalt; } public synchronized void put(int wert) { while (gefüllt == true) { try { wait();} catch (InterruptedException e) {} } inhalt = wert; gefüllt = true; notifyAll(); } }