Threadprogrammierung

Werbung
Threads in Java
Wiederholung der BS Grundlagen
Alois Schütte
AOSD
1
Threads: Erzeugen von Threads
Beim Start eines Java Programms wird ein Prozess erzeugt, der einen Thread enthält, der die
Methode main der angegebenen Klasse ausführt.
Der Code weiterer Threads muss in einer Methode mit Namen run realisiert werden.
public void run() {
// Code wird in eigenem Thread ausgeführt
}
Ein Programm, das Threads erzeugt, erbt von der Klasse Thread und überschreibt die Methode
run():
$ cat MyThread.java
public class MyThread extends Thread {
public void run() {
System.out.println("Hello World");
}
}
public static void main(String[] args) {
MyThread t = new MyThread();
t.start(); }
}
$
Alois Schütte
AOSD
2
Threads: Erzeugen von Threads
Wiederholung Interfaces
Bei Interfaces handelt es sich um eine Abart der abstrakten Klassendeklaration. Es enthält neben
Datenelementen abstrakte Methoden. Sie werden u.a. für die Mehrfachvererbung eingesetzt,
denn Klassen können mehrere Schnittstellen implementieren.
Implementiert eine Klasse ein Interface, so muss sie alle Methoden des Interface überschreiben.
Beispiel: 2 Interfaces, eine Klasse implementiert beide Interfaces und ist von einer Basisklasse
abgeleitet:
$ cat MyInterface.java
interface MyInterface1 {
String s1 = "MyInterface1";
public void print1();
}
interface MyInterface2 {
String s2 = "MyInterface2";
public void print2();
}
class MySuperClass {
protected String str = "MySuperClass ";
}
Alois Schütte
AOSD
3
Threads: Erzeugen von Threads
-> Wiederholung Interfaces
class MySubClass extends MySuperClass
implements MyInterface1, MyInterface2 {
public void print1() {
System.out.println(str + s1);
}
public void print2() {
System.out.println(str + s2);
}
}
public class MyInterfaces {
public static void main(String[] args) {
MySubClass object = new MySubClass();
object.print1();
object.print2();
}
}
$
MySubClass ist von MySuperClass abgeleitet und implementiert die beiden Schnittstellen, somit
ist der Zugriff auf alle print-Methoden möglich.
Alois Schütte
AOSD
4
Threads: Erzeugen von Threads
Threads in komplexen Klassenhierarchien
Wenn sich die Methode run() in einer Klasse befinden soll, die selbst bereits aus einer anderen
Klasse abgeleitet ist, so kann diese Klasse nicht zusätzlich von Thread abgeleitet werden (Java
unterstützt keine Implementierungs-Mehrfachvererbung).
In diesem Fall kann das Interface Runnable des Package java.lang verwendet werden:
$ cat MyRunnableThread.java
public class MyRunnableThread implements Runnable {
public void run() {
System.out.println("Hello World");
}
public static void main(String[] args) {
MyRunnableThread runner = new MyRunnableThread();
Thread t = new Thread(runner);
t.start();
}
}
$
Alois Schütte
AOSD
5
Threads: Threadtermination
Ein Thread terminiert, wenn seine run()-Methode (bzw. die Methode main() im Fall des
Ursprungs-Thread) beendet ist. Sind alle von einem Prozess initiierten Threads beendet, so
terminiert der Prozess (falls es kein Dämon ist).
Die Klasse Thread stellt eine Methode isAlive bereit, mit der abfragbar ist, ob ein Thread noch
lebt (schon gestartet und noch nicht terminiert ist). Damit könnte aktives Warten etwa wie folgt
programmiert werden (man sollte es so aber nie tun, da aktives Warten sehr rechenintensiv ist):
MyThread t = new myThread();
t.start();
while (t.isAlive());
// hier ist jetzt: t.isAlive == false, der Thread t ist terminiert
Wenn in einer Anwendung auf das Ende eines Thread gewartet werden muss, etwa um die
Rechenergebnisse des Thread weiterzuverarbeiten, kann die Methode join der Klasse Thread
benutzt werden. Der Thread wird blockiert, bis der Thread, auf den man wartet, beendet ist.
MyThread t = new myThread();
t.start();
t.join(); // blockiert, bis t beendet ist.
// auch hier ist jetzt: t.isAlive == false, der Thread t ist terminiert
Alois Schütte
AOSD
6
Threads: Abarbeitungsreihenfolge
Werden mehrere Threads erzeugt, so ist die Ausführungsreihenfolge nicht vorhersehbar!
$ cat Loop1.java
public class Loop1 extends Thread {
private String myName;
public Loop1(String name) {
myName = name;
}
public void run() {
for(int i = 1; i <= 10000; i++)
System.out.println(myName + " (" + i + ")");
}
public static void main(String[] args) {
Loop1 t1 = new Loop1("Thread 1");
Loop1 t2 = new Loop1("Thread 2");
Loop1 t3 = new Loop1("Thread 3");
t1.start();
t2.start();
t3.start();
}
}
$
Alois Schütte
AOSD
$ java Loop1
…
Thread 1 (7823)
Thread 2 (8886)
Thread 1 (7824)
Thread 2 (8887)
Thread 1 (7825)
Thread 2 (8888)
Thread 1 (7826)
Thread 3 (6647)
Thread 2 (8889)
Thread 3 (6648)
Thread 2 (8890)
Thread 3 (6649)
Thread 2 (8891)
7
Threads: Synchronisation
Problem des gemeinsamen Zugriffs
Wenn mehrere Threads gemeinsam auf Daten zugreifen, müssen sich die einzelnen Threads
darüber „verständigen“, wer wann was machen darf. Sie müssen ihre Aktivitäten synchronisieren.
Beispiel: Klasse Even stellt sicher, dass n nur gerade sein kann:
$ cat Even1.java
class Even { // POST: n is always even
private int n = 0;
public int next() {
++n;
try {
Thread.sleep(100);
} catch (InterruptedException e) {};
++n;
return n;
}
}
Alois Schütte
AOSD
8
Threads: Synchronisation
- > Problem des gemeinsamen Zugriffs
Das Programm mit einem Thread funktioniert problemlos, also ist die Klasse offensichtlich korrekt
implementiert, oder?
public class Even1 extends Thread {
private Even e;
public Even1(Even e) {
this.e = e;
}
public void run() {
for (int i = 1; i <= 10; i++) {
System.out.println("result: " + e.next());
}
}
public static void main(String[] args) {
Even e = new Even();
Even1 t1 = new Even1(e);
t1.start();
}
}
$
Alois Schütte
AOSD
$ java Even1
result: 2
result: 4
result: 6
result: 8
result: 10
result: 12
result: 14
result: 16
result: 18
result: 20
$
9
Threads: Synchronisation
- > Problem des gemeinsamen Zugriffs
Wenn mehrere Threads sich ein Even-Objekt teilen, kommt es zu unerwarteten Ausgaben
(ungeraden Werten). Wie ist das erklärbar?
public class Even2 extends Thread {
private Even e;
public Even2(Even e) {
$ java Even2
this.e = e;
result: 3
}
result: 4
public void run() {
result: 7
for (int i = 1; i <= 10; i++) {
result: 8
System.out.println("result: " + e.next()); result: 11
result: 12
}
result: 15
}
public static void main(String[] args) {
Even e = new Even();
Even2 t1 = new Even2(e);
Even2 t2 = new Even2(e);
t1.start();
t2.start();
}
}
$
Alois Schütte
AOSD
result: 16
result: 19
result: 21
result: 23
result: 24
result: 27
result: 28
result: 31
result: 33
result: 35
result: 37
result: 39
result: 40
$
10
Threads: Synchronisation
Synchronized Methoden und Blöcke
Die Java-Superklasse Object beinhaltet als Eigenschaft eine Sperre. Da jede Klasse von Object
abgeleitet ist, besitzen alle Klassen diese Eigenschaft.
Das Sperren gehorcht dem „acquire-release Protokoll“:
Die Sperre wird gesetzt (acquire), beim Betreten einer synchronized-Methode (oder einer
synchronized Blocks) und entfernt (release) beim Verlassen des Blocks
(auch beim Verlassen durch eine exception).
Sperre
Thread A
Thread B
Objekt
Wird eine Methode einer Klasse mit synchronized gekennzeichnet, so muss diese Sperre zuerst
gesetzt werden, bevor die Methode ausgeführt wird, hier initiiert von Thread A.
Hat ein anderer Thread A die Sperre bereits gesetzt (seine Methode ist in Ausführung), so wird
der aufrufende Thread B blockiert. Das Blockieren ist aber nicht durch aktives Warten realisiert,
sondern der Thread A wird beim Thread-Umschalten nicht mehr berücksichtigt.
Wenn die Methode des Thread A beendet ist, wird die Sperre entfernt und der Thread B wird
beim Scheduling wieder berücksichtigt.
Alois Schütte
AOSD
11
Threads: Synchronisation
-> Synchronized Methoden und Blöcke
Somit nun die korrekte Implementierung der Klasse Even:
$ cat Even3.java
class Even { // POST: n is always even
private int n = 0;
public synchronized int next() {
++n;
try {
Thread.sleep(100);
} catch (InterruptedException e) {};
++n;
return n;
}
}
…
Alois Schütte
AOSD
$ java Even3
result: 2
result: 4
result: 6
result: 8
result: 10
result: 12
result: 14
result: 16
result: 18
result: 20
result: 22
result: 24
result: 26
result: 28
result: 30
result: 32
result: 34
result: 36
result: 38
result: 40
$
12
Threads: Synchronisation
-> Synchronized Methoden und Blöcke
Neben der Markierung synchronized für Methoden, kann man auch einen einzelnen Block
markieren:
public void buchen(int kontonr, float betrag) {
synchronized(konten[kontonr]) {
float alterStand = konten[kontonr].abfragen();
float neuerStand = alterStand + betrag;
konten[kontonr].setzen(neuerStand);
}
}
Hier wird die Sperre auf das Objekt konten[kontonr] angewendet.
Generell gilt folgende Regel zur Verwendung von synchronized:
Wenn von mehreren Threads auf ein Objekt zugegriffen wird, wobei mindestens ein Thread
den Zustand (repräsentiert durch die Werte der Attribute) des Objekts ändert, dann müssen
alle Methoden, die auf den Zustand lesend oder schreibend zugreifen, mit synchronized
gekennzeichnet werden.
Alois Schütte
AOSD
13
Threads: wait und notify
In vielen Anwendungssituationen ist es erforderlich, dass eine Methode nur dann ausgeführt
wird, wenn zusätzlich zum konsistenten Zustand weitere anwendungsspezifische Bedingungen
erfüllt sind.
Das Prüfen dieser Bedingungen durch aktives Warten (polling) belastet die CPU intensiv und nicht
zu empfehlen. Lösung: Methoden wait und notify der Klasse Objekt
Ein wait bewirkt die folgenden Aktionen:
1. wenn der laufende Thread unterbrochen wurde, wird die Ausnahme InterruptedException
erzeugt. Andernfalls (Normalfall) wird der laufende Thread blockiert.
2. Die JVM fügt den laufenden Thread in eine Menge (wait set) ein, die mit dem Objekt
assoziiert ist.
3. Der synchronization Lock für das Objekt wird freigegeben (released), alle anderen Locks
bleiben erhalten.
Ein notify bewirkt die folgenden Aktionen:
1. Ein zufälliger Thread t wird aus dem wait set des Objektes ausgewählt.
2. Thread t muss den Lock des Objektes wieder erhalten, d.h. er blockiert solange, bis der
Thread der notify aufgerufen hat, den Lock besitzt oder bis ein anderer Thread, der den Lock
hält, ihn freigegeben hat.
3. Thread t wird nach erhalten des Lock nach seinem wait weitermachen.
Ein notifyAll arbeitet genauso, nur dass alle Threads im wait set ausgewählt werden (Achtung:
nur einer kann aber weitermachen, da die anderen ja auf den Erhalt des Lock warten).
Alois Schütte
AOSD
14
Threads: wait und notify
T1
class X {
synchronized void w() {
before();// some actons
wait(); // Thread.wait
after(); // some actions
}
begin x.w()
acuire lock
before();
wait();
release lock
enter wait set
synchronized void n() {
notify();// Thread.notify
}
T2
T3
begin x.w()
blocks
acuire lock
before();
wait();
release lock
enter wait set
void before {...}
void after {...}
begin x.n()
wait for lock
aquire lock
notify()
release lock
}
exit wait set
wait lor lock
exit wait set
wait lor lock
aquire lock
after()
release lock
aquire lock
after()
release lock
T1
T2
waiting set
Alois Schütte
AOSD
15
Threads: wait und notify
Probleme mit wait() und notify() entstehen, wenn mehrere Threads in der
Warteschlange stehen und der falsche Thread geweckt wird. Dies wird am ErzeugerVerbraucher Problem demonstriert.
buffer
producer
put()
class Buffer {
private boolean available=false;
private int data;
public synchronized void put(int x) {
while(available) {
try { wait();
} catch(InterruptedException e) {}
}
data = x;
available = true;
notify();
}
consumer
get()
public synchronized int get() {
while(!available) {
try { wait();
} catch(InterruptedException e) {}
}
available = false;
notify();
return data;
}
} // end Buffer
Alois Schütte
AOSD
16
Threads: wait und notify
class Producer extends Thread {
private Buffer buffer;
private int start;
public Producer(Buffer b, int s) {
buffer = b;
start = s;
}
public void run() {
for(int i = start; i < start + 100; i++) {
buffer.put(i);
}
}
}
class Consumer extends Thread {
private Buffer buffer;
public Consumer(Buffer b) {
buffer = b;
}
public void run() {
for(int i = 0; i < 100; i++) {
int x = buffer.get();
System.out.println("got " + x);
}
}
}
Alois Schütte
AOSD
17
Threads: wait und notify
public class ProduceConsume {
public static void main(String[] args) {
Buffer b = new Buffer();
Consumer c = new Consumer(b);
Producer p = new Producer(b, 1);
c.start();
p.start();
}
}
$ java ProduceConsume
got 1
got 2
got 3
got 4
got 5
…
got 100
$
Insgesamt ist die Ausgabe, so wie wir das erwartet haben.
Alois Schütte
AOSD
18
Threads: wait und notify
Nun werden mehrere Erzeuger und Verbraucher gestartet.
$ cat ProducerConsumer2.java
public class ProduceConsume2 {
public static void main(String[] args)
Buffer b = new Buffer();
Consumer c1 = new Consumer(b);
Consumer c2 = new Consumer(b);
Consumer c3 = new Consumer(b);
Producer p1 = new Producer(b, 1);
Producer p2 = new Producer(b, 101);
Producer p3 = new Producer(b, 201);
c1.start();
c2.start();
c3.start();
p1.start();
p2.start();
p3.start();
}
}
{
$ java ProduceConsume2
got 1
got 102
got 2
got 101
got 104
got 103
got 105
Das Programm bleibt stehen, es passiert nichts mehr; es wird keine neue Ausgabe mehr
erzeugt, das Programm ist aber noch nicht beendet. Dieses Verhalten wurde verursacht, da
durch notify() der „falsche“ Thread (ein Verbraucher) geweckt wurde.
Alois Schütte
AOSD
19
Threads: wait und notify
Lösung: notify durch notifyAll ersetzen:
class Buffer {
private boolean available=false;
private int data;
public synchronized void put(int x) {
while(available) {
try { wait();
} catch(InterruptedException e) {}
}
data = x;
available = true;
notifyAll();
}
public synchronized int get() {
while(!available) {
try { wait();
} catch(InterruptedException e) {}
}
available = false;
notifyAll();
return data;
}
} // end Buffer
Die Methode notifyAll() ist zu verwenden, wenn mindestens eine der beiden folgenden Situationen
zutrifft:
• In der Warteschlange befinden sich Threads, mit unterschiedlichen Wartebedingungen (z.B. Puffer
leer, Puffer voll). Dann kann bei Verwendung von notify() der „falsche“ Thread geweckt werden.
• Durch die Veränderung des Zustands eines Objekts können mehrere Threads weiterlaufen (Wert
im Puffer alle wartenden Verbraucher können arbeiten).
Alois Schütte
AOSD
20
Threads: Deadlocks
Synchronisation mittels synchronized verhindert keine Deadlocks:
$ cat Deadlock.java
public class Deadlock {
public static void main(String[] args) {
// resource objects to locks on
final Object resource1 = ”r1";
final Object resource2 = ”r2";
t1
r1
r2
t2
// first thread - it tries to lock resource1 then resource2
Thread t1 = new Thread() {
public void run() {
// lock resource1
synchronized(resource1) {
System.out.println("Thread 1: locked resource 1");
try { Thread.sleep(50); } // pause...
catch (InterruptedException e) {}
// attempt to lock resource2
System.out.println("Thread 1: wants resource 2");
synchronized(resource2) {
System.out.println("Thread 1: locked resource 2");
}
} // synchronized
} //run
}; // Thread 1
Alois Schütte
AOSD
21
Threads: Deadlocks
// second thread - it tries to lock resource2 then resource1
Thread t2 = new Thread() {
public void run() {
// lock resource2
synchronized(resource2) {
System.out.println("Thread 2: locked resource 2");
try { Thread.sleep(50); } // pause...
catch (InterruptedException e) {}
System.out.println("Thread 2: wants resource 1");
synchronized(resource1) {
System.out.println("Thread 2: locked resource 1");
}
} // synchronized
} // run
}; // Thread 2
// start threads - should deadlock and program will never exit
t1.start();
t2.start();
}// main
}
$
Alois Schütte
AOSD
t1
r1
r2
t2
$ java Deadlock
Thread 1: locked resource 1
Thread 2: locked resource 2
Thread 1: wants resource 2
Thread 2: wants resource 1
22
Herunterladen