Protokoll Stunde 5

Werbung
Praktikum aus Softwareentwicklung 2, Stunde 5
Lehrziele/Inhalt
1. Threads
Threads
Threads sind parallele, oder auf Rechnern mit nur einer CPU quasi-parallele, Programmabläufe in
Java. Sie können beispielsweise benutzt werden, um mehrere Anforderungen auf einem Server
abzuarbeiten, Hintergrundtätigkeiten wie Animationen durchzuführen oder lang-laufende Aufgaben
vom GUI-Thread zu entkoppeln.
Basisklassen
Java bietet folgende Basisklassen zum Umgang mit Threads:
java.lang.Thread



Thread-Objekte bilden Threads ab; und bieten programmatischen Zugriff darauf, zB: starten
(start), unterbrechen (interrupt), abgeben der Kontrolle (yield) und setzen der Priorität
(setPriority).
Hat statische Hilfsmethoden, mit denen man auf den aktuellen Thread zugreifen kann.
Auf Betriebssystemen die Threads unterstützen werden diese genutzt.
java.lang.Runable


Aufgaben die in einem Thread ausgeführt werden sollen müssen in Objekte gekapselt
werden. Diese Objekte müssen das Interface Runnable implementieren.
Pro Thread kann eine Aufgabe im Konstruktor übergeben werden.
java.lang.Object

Implementiert einen Monitor, d.h. jedes Objekt kann zur Thread-Synchronisation genutzt
werden.
java.lang. InterruptedException

Wird geworfen wenn ein Thread schläft oder wartet und von außen unterbrochen wird.
Anlegen eines Threads
Die Klasse Thread verwaltet Threads in Java. Will man einen Thread in Java starten muss man ein
Objekt dieser Klasse anlegen und darauf die Methode start aufrufen. Ein Thread-Objekt kann nur
einmal gestartet werden, sobald er seine Aufgabe abgearbeitet hat ist er tot und kann nicht mehr
verwendet werden.
Das Interface Runnable ist die Schnittstelle für Aufgaben. Runnable enthält nur die Methode void
run(). Benötigt man Parameter oder einen Rückgabewert, dann muss man diese als Felder im Objekt
ablegen.
© Markus Löberbauer 2010
Seite 12
Beispiel: Anlegen eines Threads der die Zahlen von 1 bis 100 ausgibt.
Definieren der Aufgabe als Runnable:
public class CounterTask implements Runnable {
public void run() {
for (int i = 1; i <= 100; ++i) {
System.out.println(i);
}
}
}
Anlegen und starten des Threads:
Thread counterThread = new Thread(new CounterTask());
counterThread.start();
Die Klasse Thread kann auch erweitert werden, wenn man eine spezielle Art von Threads braucht, zB
Threads die Zeitmessungen machen oder Threads die Ereignisse auslösen. Diese Erweiterbarkeit kann
auch verwendet werden, um einen Thread mit einer Aufgabe zu versehen. Allerdings ist diese Art der
Erweiterung im objektorientierten Sinn falsch. Und aus diesem Grund in anderen Programmiersprachen, wie beispielsweise C#, unmöglich.
Negativ-Beispiel: Anlegen einen Threads der die Zahlen von 1 bis 100 ausgibt, als Thread-Ableitung.
public class CounterThread extends Thread {
public void run() {
for (int i = 1; i <= 100; ++i) {
System.out.println(i);
}
}
}
CounterThread counterThread = new CounterThread();
counterThread.start();
Unterbrechen eines Threads
Es gibt Threads die ihre Aufgabe so lange ausführen bis sie von außen unterbrochen werden. Zum
Beispiel Server-Threads die Client-Anfrage abarbeiten. Einen Thread kann man zuverlässig und sicher
abbrechen lassen, indem man in der Verarbeitungs-Schleife Thread.interrupted() prüft oder ein als
volatile markiertes Feld ausliest.
Reagiert der Thread auf Thread.interrupted(), dann kann der Thread von außen über die Methode
interrupt beendet werden. Liest der Thread ein volatile Feld aus, dann kann man von außen auf
dieses Feld schreiben um den Thread zu beenden.
Es ist auch möglichen einen Thread über die Methode stop zu beenden. Dabei wird der Thread
allerdings ohne Vorwarnung gestoppt, ohne die Möglichkeit zu haben begonnene Aufgaben
abzuschließen, was zu inkonsistenten Datenmodellen führt.
Korrekter Umgang mit Thread.interrupted():
public class Exiter implements Runnable
public void run() {
© Markus Löberbauer 2010
Seite 13
while(!Thread.interrupted()) {
// Endless loop
}
}
}
oder, falls in der Endlosschleife eine InterruptedException auftreten kann
public class Exiter implements Runnable
public void run() {
while (!Thread.interrupted()) {
try {
// do something
sleep(1000); // may throw an InterruptedException
} catch (InterruptedException e) {
// Call interrupt() to set interrupted()
interrupt();
}
// finish work
}
}
}
Korrekter Umgang mit einem volatile Feld:
volatile boolean exit;
private class Exiter implements Runnable {
public void run() {
while (!exit) {
// Endless loop
}
}
}
Synchronisation
In Java nutzen alle Threads einen gemeinsamen Speicherbereich, bei gemeinsam genutzten Objekten
muss der Zugriff daher synchronisiert werden. Synchronisation kann auf Methoden- und Block-Ebene
erfolgen. Synchronisiert man auf Blockebene, dann muss explizit ein Objekt angeben werden auf das
synchronisiert werden soll. Synchronisiert man auf Methodeneben wird das this-Objekt benutzt.
Handelt es sich um eine statische Methode wird das Klassen-Objekt benutzt. Synchronisation auf
Blockebene ist flexibler, weil man bestimmen kann welches Objekt zur Synchronisation benutzt
werden soll; und sie ist sicherer, weil man das Synchronisationsobjekt lokal halten kann.
Synchronisierter Block
Synchronisierte Methode
Object obj = new Object();
synchronized void bar() {
// do critical stuff here
}
void foo() {
// uncritical stuff
synchronized(obj) {
// do critical stuff here
}
// uncritical stuff
}
© Markus Löberbauer 2010
// equivalent to
void bar() {
synchronized(this) {
// do critical stuff here
}
}
Seite 14
Bedingtes Warten
Muss in einem Thread auf eine Bedingung gewartet werden bevor weiter gearbeitet werden kann,
muss man mit dem Monitor arbeiten. Threads können auf einen Monitor warten und wartende
Threads benachrichtigen. Jedes Objekt in Java ist ein Monitor, dazu sind in Objekt die Methoden
wait, wait(timeout), wait(timeout, nanos), notify und notifyAll vorhanden.
Die Methode wait blockiert den Thread bis er über den Monitor notifiziert wird; oder der Thread mit
interrupt unterbrochen wird. Möchte man maximal nur eine gewisse Zeit warten kann man die
Methode wait(timeout) oder wait(timeout, nanos) benutzen.
Die Methode notify benachrichtigt einen Thread der auf den Monitor wartet, die Auswahl des
Threads erfolgt zufällig. Mit der Methode notifyAll werden alle wartenden Threads benachrichtigt.
Beispiel: Überweisen eines Geldbetrags. Wobei am Quellkonto genug Geld vorhanden sein muss.
public class Bank {
private Object lock = new Object();
private Account[] accounts;
// ...
public void transfer(int from, int to, int amount)
throws InterruptedException {
synchronized(lock) {
while (accounts[from] < amount) {
lock.wait();
}
accounts[from] -= amount;
accounts[to] += amount;
lock.notifyAll();
}
}
}
In diesem Beispiel sieht man warum notifyAll wichtig ist. Bevor von einem Konto etwas abgebucht
werden kann muss genügend Geld vorhanden sein. Das bedeutet eine Überweisung ist eventuell von
einer anderen Überweisung abhängig. Würde man hier nur notify verwenden könnten die Threads in
eine Blockierung geraten. Mit notifyAll haben alle Threads die Möglichkeit ihre Bedingung zu prüfen.
Warten auf einen Thread
Teilt man eine Aufgabe auf mehrere Threads auf, dann muss man, spätestens sobald man das
Ergebnis braucht, warten bis alle Threads fertig sind. Dazu kann man am Thread die Methode join
aufrufen.
Beispiel:
// start an extra thread
Thread t = new Thread(...);
t.start();
// concurrent execution
t.join();
// thread t is dead
© Markus Löberbauer 2010
Seite 15
Zustände eines Threads




neu: erzeugt aber noch nicht gestartet
lauffähig
o aktiv: wird gerade ausgeführt
o bereit: kann ausgeführt werden und wartet auf Zuteilung des Prozessors
blockiert
o schlafend: mit sleep schlafen gelegt
o IO-blockiert: wartet auf Beendigung einer IO-Operation
o wartend: wurde mit wait in den wartenden Zustand versetzt
o gesperrt: Wartet auf die Aufhebung einer Objekt-Sperre
o suspendiert: durch suspend() vorübergehend blockiert
Achtung: ist veraltet und sollte nicht verwendet werden
tot: run()-Methode ausgelaufen
blockiert (blocked)
g
un
we
is
en
d()
()
wa
it-A
n
um
e
(syn
chro
n
y
/n
o
t
i
f
yA
ll
tspe
rre
suspendiert
(suspended)
res
be
aktiv
(active)
no
tif
Aufh
e
nO
b
j
e
ktsp
erre
Ende IO-Operatio
n
chen
aufwa
p()
slee
Obje
k
wartend
(waiting)
su
sp
gesperrt
(locked)
ized)
IO-blockiert
(IO-blocked)
IO-Opertion
schlafend
(sleeping)
bereit
(ready)
neu
(new)
© Markus Löberbauer 2010
run terminiert
start()
lauffähig (runnable)
tot
(dead)
Seite 16
Herunterladen