Threads (Nebenläufigkeit)

Duke in Threads

(Lizenz)

Einleitung

Java bietet als Programmiersprache von Beginn an eine aktive Unterstützung für das Programmieren mit Threads (engl der Faden, das Fädchen). Hierdurch ist es in Java relativ einfach nebenläufige Programme zu implementieren.
Das Threadkonzept erlaubt es in der gleichen Javaanwendungen Dinge parallel abzuarbeiten und trotzdem auf die gleichen Daten zuzugreifen.

 

Hiermit ist die Implementierung von Javaanwendungen mit den folgen Vorteilen möglich:

  • man kann sehr viel mehr Aufgaben abarbeiten, da man es gleichzeitig tun kann.
  • man kann Aufgaben schneller zu erledigen, da man es parallel tun kann.
  • man kann Aufgaben asynchron, parallel im Hintergrund abarbeiten ohne andere Aufgaben warten zu lassen.

Prozesse und Threads im Betriebssystem

Mit Java-Threads kann man nebenläufige Programme programmieren dies es erlauben mehrere Dinge gleichzeitig zu tun. Ein sehr einfaches Beispiel ist eine Programm mit einer graphische Benutzeroberfläche. Dieses Programm lädt typischerweise in einem Thread eine Datei aus dem Internet während gleichzeitig ein anderer Thread auf dem Bildschirm einen Fortschrittsbalken vergrößert.

Betriebssysteme verwalten die Ressourcen eines Rechners. Beim Programmieren mit nebenläufigen Java-Threads ist es wichtig zu verstehen, wie die beiden wichtigen Resourcen Hauptspeicher und Prozessoren vom Javalaufzeitsystem und dem Betriebsystem verwaltet werden. Bei der Betrachtung des Speichers spielt nur der virtuelle Speicher des Betriebsystems eine Rolle, da der Javaentwickler in der Regel keinen direkten Einfluss auf dem physischen Speicher nehmen kann. Moderne Betriebsysteme sind in der Lage Programme gleichzeitig bzw. nebenläufig auszuführen.

Definition
Prozess(Informatik)
Ein Prozess bezeichnet in der Informatik ein im Ablauf befindliches Computerprogramm (siehe Wikipedia). Zum Prozess gehört das Programm samt seiner Daten und dem Prozesskontext (siehe Wikipedia).

 

Das Javalaufzeitsystem ist aus der Sicht des Betriebssystems während seiner Ausführung ein Prozess.

Prozesse besitzen während ihrer Ausführung typischerweise:

  • ein Programm welches sie ausführen. Mehrere Prozesse können durchaus das gleiche Programm ausführen
  • einen eigenen Speicherbereich zur Verwaltung der Daten. Das Betriebsystem verwaltet diesen Speicher und sorgt dafür das alle Prozesse ihren eigenen Speicher benutzen können ohne sich gegenseitig zu beinflussen. Bei Java ist der Heap der bekannteste gemeinsame Speicherbereich.
  • Zumindest einen Programmstack (Stapel) zur Verwaltung der Daten von Methoden und einen dazugehörige Befehlszähler/zeiger
  • Zugriff auf Betriebssystemressourcen (Bildschirm, Tastatur, Massenspeicher, Netzwerk etc.)

Threads (engl. der Faden, das Fädchen) sind leichtgewichtige Ausführungseinheiten eines Prozesses:

Definition
Thread (Informatik)
Ein Thread (auch: Aktivitätsträger oder leichtgewichtiger Prozess) bezeichnet in der Informatik einen Ausführungsstrang oder eine Ausführungsreihenfolge in der Abarbeitung eines Programms. Ein Thread ist Teil eines Prozesses. (siehe Wikipedia)

Das Javalaufzeitsystem ist typischerweise ein Prozess des Betriebssystems. Die Java-Threads werden normalerweise auf Betriebssystem-Threads abgebildet. Dies war in frühen Javaimplementierungen (1.1) nicht der Fall. Hier wurden die Threads von Java selbst verwaltet (siehe Green Threads) .

Ein Thread besitzt typischerweise

  • keinen eigenen Speicher (Heap). Er und alle anderen Threads des Prozesses haben Zugriff auf den gemeinsamen Hauptspeicher seines Prozesses. Durch das gemeinsame, gleichzeitige Arbeiten auf den gemeinsamen Daten ist der Datenaustausch zwischen Threads sehr einfach. Die Konsistenz der Daten muss jedoch aktiv verwaltet werden.
  • einen eigenen Programmstack. Er dient der Verwaltung der aktuell aufgerufenen Methodenvariablen.
  • einen eigenen Befehlszähler

Prozesse bestehen aus mindestens einem Thread (dem Haupt-Thread, Main-Thread) und eventuell zusätzlichen Threads die eigene Programmstacks zur parallelen Ausführung besitzen. Die Lebensdauer von Threads ist durch die Lebensdauer des dazugehörigen Prozesses beschränkt. Sie enden mit dem Beenden des Pozesses.

 

Nebenläufige Ausführung im Betriebssystem

Betriebsysteme weisen den Prozessen die Prozessoren zur Ausführung zu. Hierfür verwendet man die englischen Begriffe des "scheduling" oder "dispatching". Ziel des Betriebssystems ist es die Prozessoren möglichst gut auszunutzen und eine faire, vorteilhafte Abarbeitung aller Programme in Prozessen zu gewährleisten.

Die (historisch) einfachste Art der Verwaltung von Prozessen durch das Betriebsystem ist der Batchbetrieb (Stapelbetrieb). Ein Rechner hat typischerweise nur einen Prozessor. Das Betriebssystem kann nur ein Programm gleichzeitig als Prozess ausführen. Es weist dem Prozessor eine Programm A zu dieses läuft bis es beendet wird. Anschließend wird das nächste Programm ausgeführt: 

Multi tasking: Um interaktive Benutzer zu bedienen ist es geschickter laufende Prozesse zu unterbrechen und andere Prozesse teilweise abzuarbeiten. Aufgrund der hohen Prozessorgeschwindigkeit hat der menschliche Betrachter den Eindruck, die Prozesse laufen gleichzeitig. Alle moderne Betriebsysteme arbeiten nach diesem Prinzip. Das Unterbrechen der Prozesse ist oft problemlos möglich, da sie sich oft selbst blockieren. Sie müssen da si relativ lange auf Daten von Benutzern, Festplatten, dem Netzwerk warten müssen. In diesen vielen Zwangpausen kann das Betriebssyteme andere, lauffähige Prozesse abarbeiten. 

Die Prozesse laufen jetzt verschränkt und sie werden in vielen einzelnen Blöcken abgearbeitet. Sie werden quasi-parallel abgearbeitet.

Multi tasking-Multiprozessor: Da alle modernen Prozessoren mehrere Ausführungseinheiten besitzen können die Betriebsysteme Prozesse parallel abarbeiten. Die Prozesse müssen nicht zwangsweise auf dem gleichen Prozessor ausgeführt werden (siehe Beispiel). Die Gesamtausführungszeiten können bei mehren Prozessoren entsprecht verkürzen.

Multithreaded-Multiprozessor: Da Threads leichtgewichtige Prozesse mit einem Programmstack und einem eigenen Programmablauf sind, werden sie bei der Prozessorvergabe wie Prozesse behandelt.  Ein Prozess kann jetzt mehrere Prozessoren gleichzeitig verwenden wenn er nur über mehrere Theads verfügt. Anwendungen können jetzt innerhalb eines Prozesses skalieren. Dies bedeutet, sie können (threoretisch) beliebig viele Prozessoren benutzen und damit beliebig viele Aufgaben in einer bestimmten Zeit abarbeiten. Der Durchsatz eines Prozesses, bzw. die Abarbeitungsgeschwindigkeit ist nicht mehr direkt an die Geschwindigkeit eines einzelnen Prozessors gebunden.

 

 

Thread Zustandsübergänge

 Prozesse haben unterschiedliche Zustände, die von den verfügbaren Betriebsmitteln abhängen:

  • "blocked": ein Prozess der auf Daten von einem Gerät wie Festplatte, Tastatur, Maus oder Netzwerk wartet ist blockiert. Das Betriebssystem wird ihm keinen Prozessor zuweisen da er ihn nicht nutzen kann solange ihm die notwendigen Daten fehlen.
  • "ready-to-run": Der Prozess hat alle Betriebsmittel, mit Ausnahme des Prozessors, die er zum Laufen benötigt. Er wartet bis der Dispatcher einen Prozessor zuweist.
  • "running": Der Prozess hat alle nötigen Betriebsmittel inklusive eines Prozessors. Er führt sein Programm aus bis er eine nicht vorhandene Ressource benötigt oder bis er vom Dispatcher den Prozessor entzogen bekommt.

Die Übergänge zwischen den vereinfachten Zuständen eines Prozesses sind im folgenden Diagramm dargestellt:

Threads haben ähnlich wie Prozesse eine Reihe von Zuständen. Sie besitzen jedoch mehr Zustände, da ihre Lebensdauer kürzer als die des Prozesses ist und sie sich miteinandere synchronisieren müssen. Threads haben die folgenden fünf Zustände:

  • new: Der Thread wurde mit dem new Operator erzeugt. Er befindet sich im Anfangszustand. Auf seine Daten kann man zugreifen. Er ist noch nicht ablauffähig.
  • ready-to-run: Der Thread ist lauffähig und wartet auf eine Prozessorzuweisung
  • running: Der Thread hat einen Prozessor und führt das Programm aus
  • blocked: Der Thread wartet auf Ressourcen
  • dead: Der Thread kann nicht wieder gestartet werden

Eine Reihe dieser Zustände kann durch Methodenaufrufe vom Entwickler beeinflusst werden:

  • start(): Ein Thread wechselt vom Zustand "new" zu "ready-to-run" 
  • sleep(): Ein laufender Thread wird für eine bestimmte Zeit blockiert
  • join(): Ein Thread blockiert sich selbst bis der Thread dessen join() Methode aufgerufen wurde sich beendet hat
  • yield(): Ein Thread gibt freiwillig den Prozessor auf und erlaubt der Ablaufsteuerung den Prozessor einem anderen Thread zuzuweisen
  • interrupt(): Erlaubt es Threads die wegen eines sleep() oder join() blockiert sind wieder in den Zustand "ready-to-run" zu versetzen

Programmieren mit Threads

Java erlaubt das Erzeugen und Verwalten von Threads mit Hilfe der Systemklasse Thread. Beim Starten einer Javaanwendung bekommt die Methode main() automatisch einen Thread erzeugt und zugewiesen der sie ausführt. Mit Hilfe der Klasse Thread kann man selbst zusätzliche Threads erzeugen und starten.

Erzeugen und Start mit der Klasse Thread

Man kann einen neuen Thread starten indem man ein Objekt von Thread erzeugt. Hiermit wird parallel im Hintergrund ein Javathread erzeugt. Das Aufrufen der Methode start() startet dann den neuen Thread. Dies geschieht wie im folgenden Beispiel gezeigt:

public static void main(String[] args {
   ...
   Thread t1= new Thread(...);
   t1.start();
   ...
}

Im Laufzeitsystem wird hierdurch zuerst ein neuer Thread erzeugt und dann gestartet:

 

Es verbleibt die Frage welchen Programmcode der neue Thread ausführt.

Der ausgeführte Programmcode steht in einer Methode mit den Namen run() und muss von einer Klasse nach den Vorgaben der Schnittstelle Runnable implementiert werden.

Hierfür muss beim Erzeugen des Thread-objekts eine Referenz auf ein Objekt mitgegeben werden welches diese Schnittstelle implementiert. Geschieht dies nicht wird die Methode run() des Tread-objekts aufgerufen. Hierdurch ergeben sich zwei Möglichkeiten einen eigenen Thread mit einem bestimmten Programm zu starten:

Starten eines Threads durch Erweitern der Klasse Thread

Die erste Möglichkeit besteht im Erweitern der Klasse Thread und im überschreiben der Methode run(). Im unten aufgeführten Beispiel wurde die Klasse myThread aus der Klasse Thread abgeleitet:

Die Klasse myThread verwaltet die Threads in ihrer main() Methode. Das Erzeugen und starten der Threads der Klasse myThread könnte auch aus jeder anderen beliebigen Klasse erfolgen.

package Kurs2.Thread;

public class myThread extends Thread {
public void run() {
System.out.println("Hurra ich bin myThread in einem Thread mit der Id: "
+ Thread.currentThread().getId());
}

public static void main(String[] args) {
System.out.println("Start myThread.main() Methode im Thread mit der Id: "
+ Thread.currentThread().getId());
myThread t1 = new myThread();
t1.start();
System.out.println("Ende myThread.main() Methode im Thread mit der Id: "
+ Thread.currentThread().getId());
myThread t2 = new myThread();
// t2 ist zwar ein Threadobjekt und repräsentiert einen Thread
// da das Objekt nicht mit start() aufgerufen läuft es im gleichen
// Thread wie die main() Routine!
t2.run();
}
}

Das Programm erzeugt die folgende Konsolenausgabe:

Start myThread.main() Methode im Thread mit der Id: 1
Hurra ich bin myThread in einem Thread mit der Id: 10
Ende myThread.main() Methode im Thread mit der Id: 1
Hurra ich bin myThread in einem Thread mit der Id: 1

Im Beispiel Programm wird für die Referenz t1 ein neues Threadobjekt erzeugt. Anschliesend wird es durch seine start() Methode in einem eigenen Thread gestartet. Die run() Methode wird nach dem Starten im eigenen Thread ausgeführt.

Das Objekt mit der Referenz t2 ist zwar auch ein Thread. Das es aber direkt mir der run() Methode aufgerufen wird, läuft es i gleichen Thread wie die main() Methode.

Im UML Sequenzdiagramm ergibt sich der folgende Ablauf:

Starten eines Threads durch Implementieren der Schnittstelle Runnable

Das Erweitern einer Klasse aus der Klasse Thread ist nicht immer möglich. Man kann einen Thread auch starten indem man ein Thread-objekt erzeugt und ihm die Referenz auf eine Instanz der Schnittstelle Runnable mitgibt. Die Programmierabfolge ist dann:

  • Erzeugen eine Threadobjekts mit Referenz auf eine Instanz von Runnable.
  • Aufrufen der start() Methode des Threadobjekts
    • die Methode run() der Runnable-objekts wird automatisch aufgerufen

Die Klasse myRunnable:

package Kurs2.Thread;

public class myRunnable implements Runnable {

public void run() {
System.out.println("Hurra ich bin myRunnable in einem Thread mit der Id: "
+ Thread.currentThread().getId());
}

}

Die Klasse ThreadStarter die als Hauptprogramm dient:

package Kurs2.Thread;

public class ThreadStarter{
public static void main(String[] args) {
System.out.println("Start ThreadStarter.main() Methode im Thread mit der Id: "
+ Thread.currentThread().getId());
myRunnable r1 = new myRunnable();
myRunnable r2 = new myRunnable();
Thread t1 = new Thread(r1);
Thread t2 = new Thread(r2);
t1.start();
System.out.println("Ende ThreadStarter.main() Methode im Thread mit der Id: "
+ Thread.currentThread().getId());
// r2 ist zwar ein Runnableobjekt , da das Objekt aber nicht von einem
// Threadobjekt indirekt aufgerufen wirdläuft es im gleichen
// Thread wie die main() Routine!
r2.run();
}
}

Das Programm erzeugt die gleichen Ausgaben wie das vorherige Programm:

Start ThreadStarter.main() Methode im Thread mit der Id: 1
Hurra ich bin myRunnable in einem Thread mit der Id: 10
Ende ThreadStarter.main() Methode im Thread mit der Id: 1
Hurra ich bin myRunnable in einem Thread mit der Id: 1

Das Aufrufen von r2.run() startet wiederum keinen eigenen Thread. Der Vorteil der Benutzung der Schnittstelle Runnable liegt darin, dass man die Methode run() in jeder beliebigen Klasse implementieren kann.

Die wichtigsten Methoden der Klasse Thread

  • Konstruktor: Thread(Runnable target) : Erzeugt einen neuen Thread und übergibt ein Objekt dessen run() Methode beim Starten des Threads aufgerufen (anstatt die eigene run() Methode aufzurufen.
  • static Thread currentThread(); liefert den aktuellen Thread der ein Codestück gerade ausführt
  • long getId(): Liefert die interne Nummer des Threads
  • join(): Hält den aktuellen Thread an bis der referenzierte Thread beendet ist.
  • static void sleep(long millis): Lässt den aktuellen Thread eine Anzahl von Millisekunden schlafen.
  • start(): Lässt die VM den referenzierten Thread starten. Dieser ruft dann die run() Methode auf.

Synchronisation

Da Threads auf die gleichen Objekte auf im Heap zugreifen, können sie so sehr effizient Daten austauschen. Es besteht jedoch das Risiko der Datenkorruption, da man oft mehrere Daten gleichzeitig verändern muss um sie von einem konsistenten Zustand in den nächsten konsistenten Zustand zu überführen. 

Die Sitzplatzreservierung in einem Flugzeug ist hierfür ein typisches Beispiel:

Mehrere Reisebüros prüfen ein Flugzeug auf die Verfügbarkeit von 10 Plätzen für eine Reisegruppe. Ergibt das Lesen der Belegungsvariable 20 freie Plätze, so fährt Reisebüro 1 fort und liest weitere Daten um die Buchung vorzubereiten. Verzögert sich die endgültige Buchung so kann es vorkommen, dass ein zweites Reisebüro die Verfügbarkeit von 15 Plätzen abfragt und die 15 Plätze bucht. Das zweite Reisebüro erhöht den Belegungszähler also um 15 Plätze.

Kommt das erste Reisebüro nun endlich mit seiner Buchung vorran und erhöht die ursprünglich ausgelesene Variable um 10 ergibt sich ein inkonsistenter Zustand.

Man muss also in Systemen mit parallelen Zugriff auf Daten die Möglichkeit schaffen nur einen Thread über eine gewisse Zeit auf einem Datensatz (Objekt) arbeiten zu lassen um wieder einen konsistenten Zustand herzuführen.

Darf nur ein Thread gleichzeitig auf einer Variablen arbeiten, so nennt man diese eine kritische Variable. Die Zeit die ein Thread mit der Bearbeitung einer solchen Varriablen verbringt nennt man den kritischen Abschnitt oder auch den kritischen Pfad.

Das oben geschilderte Problem beim gleichzeitigen Zugriff nennt man auch "Reader/Writer" Problem, da das Lesen und Schreiben auf dem Datum atomar erfolgen muss. Da in in nebenläufigen Systemen diese Datenkorruption ausschlieslich von der Geschwindigkeit und dem zufälligen paralleln Zugriff abhängt nennt man ein solches Problem auch eine "Race Condition". Die Datenkorruption tritt zufällig und abhängig von der Ausführungsgeschwindigkeit auf.

Beispiel: Nichtatomares Inkrement in Java

Der ++ Operator ist Java ist nicht atomar. Dies bedeutet, dass zwei Threads einen bestimmten Wert auslesen können und der erste Thread schreibt den um 1 erhöhten Wert zurück während eventuell der zweite Thread noch etwas Zeit benötigt. Schreibt der zweite Thread dann den gleichen inkrementierten Wert zurück wurde die Zahl nur einmal inkrementiert. Es liegt eine Datenkorruption vor.

Im Programm ParaIncrement wird eine gemeinsame Variable zaehler von zwei Threads gleichzeitig inkrementiert. Der Wert der Vriablen sollte immer doppelt so groß wie die Anzahl der Durchläufe (Konstante MAX) eines einzelnen Threads sein.Die Korruption im Programm ParaIncrement findet relativ selten selten statt. Man muss die Konstante K für die Anzahl der Durchläufe eventuell abhängig vom Rechner und der Java virtuellen Maschine anpassen:

package Kurs2.Thread;

public class ParaIncrement extends Thread {
public static int zaehler=0;
public static final int MAX= Integer.MAX_VALUE/10;

public static void increment() {
zaehler++;
}

/**
* Starten des Threads
*/
public void run() {
for (int i=0; i < MAX; i++) {
increment();
}
}

public static void main(String[] args) {
ParaIncrement thread1 = new ParaIncrement();
ParaIncrement thread2 = new ParaIncrement();
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
}
if ((2* ParaIncrement.MAX) == ParaIncrement.zaehler)
System.out.println("Korrekte Ausführung: " +
+ ParaIncrement.zaehler);
else
System.out.println("Fehler! Soll: " + (2* ParaIncrement.MAX) +
"; Ist: " +ParaIncrement.zaehler);
}
}

Wechselseitiger Ausschluss

Zur Vermeidung der oben genannten Korruption ist es wichtig sicher zustellen, dass nur ein Thread gleichzeitig Zugriff auf diese Daten hat.

Definition
Kritischer Abschnitt/ Kritischer Pfad
Ein kritischer Abschnitt ist eine Folge von Befehlen, die ein Thread nacheinander vollständig abarbeiten muss, auch wenn er vorübergehend die CPU and einen anderen Thread abgibt. Kein anderer Thread darf einen kritischen Abschnitt betreten, der auf die gleichen Variablen zugreift, solange der erstgenannte Thread mit der Abarbeitung der Befehlsfolge noch nicht fertig ist. (siehe: Goll, Seite 744)

Sperren durch Monitore mit dem Schlüsselwort synchronized

Um die oben gezeigten Möglichkeiten von Korruptionen zu vermeiden verfügen Javaobjekte über Sperren die Monitore genannt werden. Jedes Javaobjekt besitzt einen Monitor der gesetzt ist oder nicht gesetzt ist.

  • Ein Monitor wird gesetzt wenn eine Instanzmethode des Objekts aufgerufen wird, die mit dem Schlüsselwort synchronized versehen ist.
  • Kein anderer Thread kann eine synchronisierte Instanzmethode des gleichen Objekts aufrufen, solange der Thread der den Monitor erworben hat noch in der synchronisierten Methode arbeitet.
    • Alle anderen Threads werden beim Aufruf einer synchronisierten Instanzmethode des gleichen Objekts blockiert und müssen warten bis der erste Thread die synchronisierte Methode verlassen hat.
  • Nach dem der erste Thread die synchronisierten Methode verlassen hat, wird der Monitor wieder freigegeben.
  • Der nächste Thread, der eventuell schon wartet kann den Monitor erwerben.

Es ist wichtig zu verstehen, dass in Java immer die individuellen Objekte mit einem Monitor geschützt sind. Sind zum Beispiel die Sitzplätze eines Flugzeuges durch Java-Objekte implementiert, so kann man mit der gleichen synchronisierten Methode auf untrschiedlichen Objekte parallel arbeiten.

Am Beispiel der Klasse Sitzplatz kann man sehen wie man den Monitor für einen bestimmten Sitzplatz setzen kann:

package Kurs2.Thread;

public class Sitzplatz {
private boolean frei = true;
private String reisender;

boolean istFrei() {return frei;}

/**
* Buche einen Sitzplatz für einen Kunden falls er frei ist
* @param kunde Name des Reisenden
* @return erfolg der Buchung
*/
synchronized boolean belegeWennFrei(String kunde) {
boolean erfolg = frei; // Kein Erfolg wenn nicht frei
if (frei) {
reisender = kunde;
frei = false;
}
return erfolg;
}

}

Die Methode belegeWennFrei() kann jetzt nur noch von einem Thread auf einem Objekt gleichzeitig aufgerufen werden. Die Methode istFrei() ist nicht synchronisiert und in einer parallelen Umgebung nicht sehr relevant. Man kann sich nicht darauf verlassen, dass bei der nächsten Operation der freie Zustand noch gilt.

Wichtig
Monitore und Schutz von Daten

Monitore schützen nur die synchronsierten Methoden eines Objekts. Dies bedeutet

  • Nicht synchronsierte Methoden der Klasse können weiterhin parallel aufgerufen werden
  • Die Attribute einer Objektinstanz sind nicht selbst geschützt. Man schützt Sie indirekt mit dem Schlüsselwort private. Hierdurch ist der Zugriff auf die Attribute auf die eigenen Methoden beschränkt. Die Methoden der Klasse können wiederum mit synchronized geschützt werden.

 

Statische synchronisierte Methoden

Das Schlüsselwort synchronized kann auch verwendet werden um einen Monitor für die Klasse zu setzen. Dieser Monitor hat aber keinen Einfluss auf den Zugriff auf die Objekte einer Klasse! Er schützt nur statische Methoden der Klasse.

Beispiel: Man kann das Korruptionsproblem in der Klasse ParaIncrement beheben in dem man die statische  Methode increment() synchronsiert:

public static synchronized void increment() {
        zaehler++;
    }

Die Variable zaehler ist in diesem Beispiel eine statische Variable. Sie gehört nicht zu einem der beiden erzeugten Objekten.

Synchronisierte Blöcke

Man muss nicht notwendigerweise eine gesamt Methode synchroniseren. Java bietet auch die Möglichkeit einzelne Blöcke zu synchronisieren. Das Synchronsieren eines Blocks erfolgt ebenfalls mit Hilfe des Schlüsselworts synchronized. Hier muss man jedoch das Objekt angeben für welches man einen Monitor erwerben will.

Man kann die Sitzplatzreservierung auch mit Hilfe eines synchronisierten Blocks implementieren:

boolean belegeWennFrei(String kunde) {
        boolean erfolg;
        synchronized (this) {
            erfolg =  frei; // Kein Erfolg wenn nicht frei
            if (frei) {
                reisender = kunde;
                frei = false;
            }
        }
        return erfolg;
    }

In dieser Implementierung kann die Methode belegeWennFrei() parallel aufgerufen werden. Beim Schlüsselwort synchronized muss jedoch für das aktuelle Objekt der Monitor erworben werden bevor der Thread fortfahren kann. 

Referenzen

  • Goll,Heinisch, Müller-Hoffman: Java als erste Programiersprache, Teubner Verlag
  • IBM Developerworks: Going Atomic: Gute Erklärung zu synchronisierten Zählern (AtomicInteger)

Aufgaben (Threads)

Programmieraufgabe JoinAndSleep

Ziel der Aufgabe ist es drei Threads zu programmieren die auf das Beenden des anderen Warten und dann eine Zeit schlafen:

  1. Sie drucken jeden neuen Zustand auf der Konsole aus
  2. Als erstes nach ihrem Start warten sie bis ein anderer Thread auf den sie zeigen sich beendet hat. Zeigen sie auf keinen anderen Thread so gehen sie sofort über zum nächstens Schritt.
  3. Die Threads schlafen für eine vorgegebene Zeit in ms
  4. Die Threads beenden sich

Die geforderte Aufgabe soll in einer Klasse implementiert werden

  1. Erweitern Sie die Klasse JoinAndSleep aus der Klasse Thread
  2. Attribute: Die Klasse hat ein Ganzzahlattribut sleep zur Verwaltung der Schlafzeit
    1. Die Klasse hat ein Ganzzahlattribut sleep zur Verwaltung der Schlafzeit
    2. Die Klasse hat eine Referenz auf ein Objekt der Klasse JoinAndSleep
  3. Konstruktor
    1. Der Konstruktor der Klasse erlaubt es die Schlafzeit zu übergeben und eine Referenz auf einen anderen Thread
  4. run() Methode: Diese Methode implementiert die oben genannte Semantik zum Warten und Schlafen
    1. Falls ein Thread gegeben ist soll auf sein Ende gewartet werden
    2. Anschliesend soll eine bestimmte Zeit geschlafen werden
    3. Fügen Sie zwischen allen Schritten Konsolenausgaben ein um den Fortschritt zu kontrollieren. Geben Sie hier immer auch den aktuellen Thread aus!
  5. main() Methode
    1. Erzeuge Thread 3: Er soll auf keinen Thread warten und dann 4000ms schlafen
    2. Erzeuge Thread 2: Er soll auf Thread 3 warten und dann 3000ms schlafen
    3. Erzeuge Thread 1: Er soll auf Thread 2 warten und dann 2000ms schlafen
    4. Starten Sie Thread 1
    5. Starten Sie Thread 2
    6. Starten Sie Thread 3

Hinweise:

Ich welchen Thread bin ich gerade?

  • Die statische Methode Thread.currentThread() liefert einen Zeiger auf den aktuellen Thread
  • Diesen kann man direkt ausdrucken

Wie warte ich auf einen anderen Thread?

Die Methode Thread.join() erlaubt auf das Beenden eines anderen Threads zu warten. Man muss auf eine InterruptedException vorbereitet sein, da man aufgeweckt werden kann:

Thread aThread;
...
try {
    aThread.join();
   } catch (InterruptedException e) {}

Wie lasse ich einen Thread schlafen?

Die Methode Thread.sleep() ist eine statische Methode. Man muss seinen eigenen Thread nicht kennen um ihn ruhen zulassen! Auch diese Methode kann eine InterruptedException werfen und muss mit einer Ausnahmebehandlung versehen werden:

try {
    Thread.sleep(schlafen);
   } catch (InterruptedException e) {}

Verständnisfragen

  • Wer wartet hier auf wen?
  • Ist dies an den Konsolenaufgaben zu erkennen?
  • Woran erkenne ich bei den Konsolenausgaben, das der Code in einem eigenen Thread läuft?

 

 

 

Lösungen (Threads)

Programmieraufgabe JoinAndSleep

package Kurs2.Thread;
public class JoinAndSleep  extends Thread{
    Thread joinIt;
    int schlafen;

    public JoinAndSleep(int sleeptime, Thread toJoin) {
        joinIt = toJoin;
        schlafen = sleeptime;
        System.out.println("Thread: " + this + " erzeugt");
    }

    public void run() {
        System.out.println("Thread: " +Thread.currentThread() + " gestartet");
        try {
            if (joinIt!=null) {
                joinIt.join();
                System.out.println("Thread: " +Thread.currentThread()
                        + " join auf " + joinIt + " fertig");
            }
        } catch (InterruptedException e) {}
        System.out.println("Thread: " +Thread.currentThread() 
                + " schlaeft jetzt fuer " + schlafen + "ms");
        try {
            Thread.sleep(schlafen);
        } catch (InterruptedException e) {}
        System.out.println("Thread: " +Thread.currentThread() + " endet");
    }

    public static void main (String[] args) {
        JoinAndSleep s3= new JoinAndSleep(2003, null);
        JoinAndSleep s2= new JoinAndSleep(2002, s3);
        JoinAndSleep s1= new JoinAndSleep(2001, s2);
        s1.start();
        s2.start();
        s3.start();
    }
}

 

Beispiel: Kritischer Pfad

Das hier benutzte Beispielprogramm visualiert 15 Threads die sich auf einem synchronisierten Objekt serialisieren.

Threads in grüner Farbe befinden sich nicht im kritischen Pfad. Threads in roter Farbe befinden sich im kritischen Pfad.

Der kritische Pfad in in der Klasse EinMonitor in der Methode buchen() implementiert.

Die einzige Instanz von EinMonitor verfügt über zwei Variablen a und b die Konten darstellen sollen.

Die Methode buchen() "verschiebt" mehrfach einen Betrag zwischen den beiden Konten. Die Summe der beiden Variablen sollte am Ende der Methode stets gleich sein.

Die Methode buchen() enthält etwas Overhead zur Visualierung des Konzepts:

  • am Anfang und am Ende  muss das GUI Subsystemen über den Eintritt und das Verlassen des kritischen Pfads informiert werden
  • zwischen allen Buchungen werden kleine Schlafpausen eingelegt um die Zeit im kritischen Pfad künstlich zu verlängern

Die Methode buchen() enthält eine Konsistenzprüfung am Ende der Methode die bei einem Fehler in der Buchführung eine Konsolenmeldung ausdruckt. Sie kommt in der auf dieser Seite gezeigten Variante nicht zum Zuge!

Die Klasse MainTest dient zum Starten des Programms. Sie erzeugt und startet die 15 Threads. Jeder Thread führt nur eine gewisse Anzahl von Buchungen durch und beendet sich dann.

Aufgaben

  • Übersetzen Sie die Klassen und starten Sie das Hauptprogramm
  • Der Schieberegler erlaubt das Einstellen der Schlafpausen im kritischen Pfad. Was geschieht wenn der kritische Pfad verkürzt wird?
  • Entfernen die das Schlüsselwort synchronized in der Methode buchen(). Was geschieht?
  • Was würde geschehen geschehen wenn die künstlichen sleep Aufrufe entfernt werden?
    • Hinweis: Sie müssen dann auch die Anzahl der Durchläufe pro Thread stark erhöhen. Da die Zeit im kritischen Pfad sehr kurz wird.
  • Was geschieht wenn man den yield() Aufruf für in der run() Methode von MainTest entfernt

Kommentar

Da die aktuelle Ausführungsgeschwindigkeit 4-5 Zehnerpotenzen jenseits der menschlichen Wahrnehmungsfähigkeit liegt ist es sehr schwer die echten Abläufe im Zeitlupentempo zu visualisieren. Ein künstlicher sleep() Aufruf blockiert den Prozess und gibt den Prozessor an das Betriebssystem zurück. Der Scheduler des Betriebssystems trifft bei dieser künstlichen Verlangsamung eventuell andere Entscheidungen in Bezug auf den Thread den er ausführt. Das gleiche Problem besteht beim Debuggen von Javaprogrammen. Durch das Bremsen bestimmter Threads können existierende Fehler nicht mehr reproduzierbar sein oder bisher nicht aufgetretene Fehler in der Synchronsiation können sichtbar werden.

Klasse MainTest

Hauptprogramm der Anwendung.

package Kurs2.Thread;

public class MainTest extends Thread {

public static final int INCRITICALPATH = 0;
public static final int NOTINCRITICALPATH = 1;
public static final int ENDED = 2;
public static int anzahlThreads = 15;
public static MainTest[] mt;
public int threadStatus = NOTINCRITICALPATH;
private static EinMonitor myMonitor;
public static int sleepPeriod = 500;
public int meineID;
public static ThreadingPanel tp;
public boolean stop = false;

public MainTest(int id) {
meineID = id;
}

public void run() {
long anfangszeit = System.nanoTime();
System.out.println("Thread [" + meineID + "] gestartet");
//GUIupdate(NOTINCRITICALPATH);
for (long i = 0; i < 200; i++) {
Thread t = Thread.currentThread();
// Erlaube anderen Threads die CPU zu holen
t.yield();
myMonitor.buchen(10);

}
threadStatus = ENDED;
System.out.println("Thread [" + meineID + "] beendet...");
}

public static void main(String[] args) {
// Anlegen des Monitorobjekts
myMonitor = new EinMonitor(1000000L);
mt = new MainTest[anzahlThreads];
ThreadFenster tg = new ThreadFenster();
tp = tg.tp;
// Erzeuge die Threads
for (int i = 0; i < anzahlThreads; i++) {
mt[i] = new MainTest(i);
}
// Starte die Threads
for (int i = 0; i < anzahlThreads; i++) {
mt[i].start();
}
}
}

Klasse EinMonitor

package Kurs2.Thread;

public class EinMonitor {

long invariante;
long a;
long b;

public EinMonitor(long para) {
invariante = para;
a = para;
b = 0L;
}

synchronized public void buchen(long wert) {
GUIupdate(MainTest.INCRITICALPATH);
sleepABit(MainTest.sleepPeriod/5);
this.a = this.a - wert;
sleepABit(MainTest.sleepPeriod/5);
this.b = this.b + wert;
sleepABit(MainTest.sleepPeriod/5);
this.a = this.a + wert;
sleepABit(MainTest.sleepPeriod/5);
this.b = this.b - wert;
sleepABit(MainTest.sleepPeriod/5);
GUIupdate(MainTest.NOTINCRITICALPATH);

if ((a+b) != invariante)
System.out.println("Inkonsistenter Zustand");
}

private void sleepABit(int sleep) {
try {
Thread.sleep(sleep);
} catch (InterruptedException e) {}
}

private void GUIupdate(int status) {
MainTest t = (MainTest) Thread.currentThread();
t.threadStatus = status;
t.tp.repaint();
}
}

Klasse ThreadFenster

package Kurs2.Thread;

import java.awt.BorderLayout;
import java.awt.Container;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JSlider;
import javax.swing.JTextField;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

public class ThreadFenster {

private JFrame hf;
private JButton okButton;
private JButton exitButton;
JTextField threadDisplay;
private static int SLEEPMIN = 1;
private static int SLEEPMAX = 2000;
private static int SLEEPINIT = 500;
private int threadCurrent = 10;
public ThreadingPanel tp;

public class exitActionListener implements ActionListener {

public void actionPerformed(ActionEvent e) {
System.exit(0);
}
}

public ThreadFenster() {
JPanel buttonPanel;
// Erzeugen einer neuen Instanz eines Swingfensters
hf = new JFrame("Thread Monitor");

//Nicht Beenden bei Schliesen des Fenster
hf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

// Anlegen der Button
exitButton = new JButton("Beenden");

JLabel threadsLabel = new JLabel("sleep(ms):");
JSlider threadSlider = new JSlider
(JSlider.HORIZONTAL, SLEEPMIN, SLEEPMAX, SLEEPINIT);
threadDisplay = new JTextField();
threadDisplay.setText(Integer.toString(threadCurrent));
threadDisplay.setColumns(4);
threadDisplay.setEditable(false);

threadSlider.addChangeListener(new ChangeListener() {
public void stateChanged(ChangeEvent e) {
JSlider source = (JSlider) e.getSource();
if (!source.getValueIsAdjusting()) {
MainTest.sleepPeriod = source.getValue();
threadDisplay.setText(Integer.toString(MainTest.sleepPeriod));
}
}
});

exitButton.addActionListener(new exitActionListener());

//Aufbau des Panels
buttonPanel = new JPanel();
buttonPanel.add(threadsLabel);
buttonPanel.add(threadSlider);
buttonPanel.add(threadDisplay);
buttonPanel.add(exitButton);
tp = new ThreadingPanel();
// Aubau des ContentPanes
Container myPane = hf.getContentPane();
myPane.add(buttonPanel, BorderLayout.SOUTH);
myPane.add(tp, BorderLayout.CENTER);

JMenuBar jmb = new JMenuBar();
JMenu jm = new JMenu("Ablage");
jmb.add(jm);
JMenuItem jmi = new JMenuItem("Beenden");
jmi.addActionListener(new exitActionListener());
jmi.setEnabled(true);
jm.add(jmi);
hf.setJMenuBar(jmb);

//Das JFrame sichtbar machen
hf.pack();
hf.setVisible(true);
hf.setAlwaysOnTop(true);
}
}

Klasse ThreadPanel

package Kurs2.Thread;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import javax.swing.JPanel;

/**
*
* @author sschneid
*/
public class ThreadingPanel extends JPanel {

private int ziffernBreite = 10; // Breite einer Ziffer in Pixel
private int ziffernHoehe = 20; // Hoehe einer Ziffer in Pixel

public ThreadingPanel() {
setPreferredSize(new Dimension(200, 100));
setDoubleBuffered(true);
}

/**
* Methode die das Panel überlädt mit der Implementierung
* der Baumgraphik
* @param g
*/
public void paintComponent(Graphics g) {
super.paintComponent(g);
int maxWidth = getWidth();
int maxHeight = getHeight();
for (int i = 0; i < MainTest.anzahlThreads; i++) {
g.setColor(Color.black);
g.drawString("Anzahl threads: " + MainTest.anzahlThreads, 10, 20);
paintThread(g, i, 20 + 25 * i, 30);
}
}

/**
* Malen eines Threads und seines Zustands
* @param g Graphicshandle
* @param k zu malender Thread
* @param x X Koordinate des Thread
* @param y Y Koordinate des Thread
*/
public void paintThread(Graphics g, int id, int x, int y) {
int xOffset = 1; // offset Box zu Text
int yOffset = 7; // offset Box zu Text

if (MainTest.mt[id] != null) {
if (MainTest.mt[id].threadStatus == MainTest.ENDED) {
g.setColor(Color.LIGHT_GRAY);
}
if (MainTest.mt[id].threadStatus == MainTest.NOTINCRITICALPATH) {
g.setColor(Color.GREEN);
}
if (MainTest.mt[id].threadStatus == MainTest.INCRITICALPATH) {
g.setColor(Color.RED);
}
}
int breite = 2 * ziffernBreite;
int xNextNodeOffset = 20;
int yNextNodeOffset = ziffernHoehe * 6 / 5; // Vertikaler Offset zur naechsten Kn.ebene
g.fillRoundRect(x - xOffset, y - yOffset, breite, ziffernHoehe, 3, 3);
g.setColor(Color.black); // Schriftfarbe
g.drawString(Integer.toString(id), x + xOffset, y + yOffset);
}
}


 

Lernziele (Threading)

(Lizenz)

Am Ende dieses Blocks können Sie:

  • ... zwischen einem Thread und einem Prozess unterscheiden
  • ... die Zustände eines Threads nennen und wissen wie man Threads von einem Zustand in einen anderen Zustand überführt
  • ... mit dem Schlüsselwort synchronized Objekte in kritischen Abschnitten sperren um Threads zu synchronisieren
  • die wichtigsten Methoden der Javaklasse Thread anwenden. Hierzu gehören
    • start()
    • run()
    • sleep()
    • join()
    • getId()
    • currentThread()
    • Konstruktor

Lernzielkontrolle

Sie sind in der Lage die folgenden Fragen zu beantworten: