Threads - javalink.ch

Werbung
FHZ
Hochschule für Technik+Architektur Luzern
Abteilung Informatik, Fach Programmieren
K33 THREADS
K33 THREADS
1 Nebenläufigkeit
Unter Nebenläufigkeit versteht man das gleichzeitige oder quasigleichzeitige Ablaufen von Aktivitäten im Rechner. Ersteres ist
aber nur mit Hilfe von mehreren Rechnern oder Prozessoren möglich. Diese Aktivitäten bzw. Kontrollflüsse heissen in Java "Programmfäden" oder Threads.
Threads gehören zu den fortgeschrittenen Techniken der Programmierung. Die Thread-Programmierung bringt folgende Vorteile mit sich:
§
einfache Programmierung von Animationen
§
einfacheres Programm-Design
§
§
unabhängige Probleme können nebeneinander bearbeitet werden
§
gilt speziell für interaktive Programme
schnellere Programmausführung bei echter Parallelität
K33_Threads-L, V20
© H. Diethelm
Seite 1/36
FHZ
Hochschule für Technik+Architektur Luzern
Abteilung Informatik, Fach Programmieren
K33 THREADS
Hinter der Java-Kulisse sind auch System-Threads aktiv, z.B.
§
für Garbage-Collection
§
für Event-Handling (Benutzer-Eingaben)
§
der implizite main()-Thread
2 Programme, Prozesse und Threads
Die Begriffe Programm, Prozess und Thread meinen nicht dasselbe und müssen differenziert betrachtet werden:
§ Ein Programm bezeichnet eine statische Liste von Anweisungen.
§ Unter einem Prozess versteht man ein Programm oder Teile eines Programms, die gerade ausgeführt werden; sie sind aktiv.
Ein Prozess ist eine aktive Verarbeitungseinheit.
§ Ein Thread ist eine unabhängige aktive Verarbeitungseinheit,
die im gleichen Prozesskontext läuft. Man spricht auch von
Coroutinen. Der Prozesskontext (Speicher, I/O) kann von allen
Threads benutzt werden (vgl. Synchronisierung).
K33_Threads-L, V20
© H. Diethelm
Seite 2/36
Abteilung Informatik, Fach Programmieren
FHZ
Hochschule für Technik+Architektur Luzern
K33 THREADS
Die meisten Programmiersprachen bieten keine eingebaute Unterstützung für Threads. Threads können dann gegebenenfalls via
Betriebssystemaufrufe realisiert werden, falls das Betriebssystem
Threads unterstützt. In Java dagegen sind Threads fester Bestandteil der Sprache (vgl. JVM) und nahtlos in das Konzept der
Objektorientierung eingebaut.
Je nach Betriebssystem kann die Anzahl Threads pro Prozess unterschiedlich sein:
In Fällen, wo echte Parallelität nicht möglich ist, simuliert ein
Scheduler die Parallelität, indem er den verschiedenen Threads
in gewissen Abständen den Prozessor zuteilt und gegebenenfalls
wieder entzieht. Im Zusammenhang mit Java sind folgende zwei
Scheduler-Strategien von Bedeutung:
§
reines Priority Scheduling
§
Priority Scheduling mit Time-Slicing
Bei beiden Verfahren handelt es sich um sogenanntes Preemptive-Scheduling, weil die JVM in der Lage ist, einen laufenden
Thread zu unterbrechen.
Beim ersten Verfahren wird ein Thread so lange wie möglich
ausgeführt, d.h. bis er sich selber "zurückzieht" oder bis er von eiK33_Threads-L, V20
© H. Diethelm
Seite 3/36
Abteilung Informatik, Fach Programmieren
FHZ
Hochschule für Technik+Architektur Luzern
K33 THREADS
nem "wichtigeren" Thread (vgl. Thread mit höherer Priorität) verdrängt wird.
Beim zweiten Verfahren gelangen alle "willigen" Threads mit der
höchsten Priorität periodisch für eine bestimmte Zeit zur Ausführung. Dieses Verfahren ist allerdings nichtdeterministisch,
weil zu einem bestimmten Zeitpunkt nicht eindeutig ist, welcher
Thread gerade ausgeführt wird.
Die Java-Spezifikation schreibt nicht zwingend ein bestimmtes
Scheduling-Verfahren für eine JVM vor!!
3 Threads in Java
3.1 Mittels der Klasse Thread
In Java sind alle Threads Instanzen bzw. Objekte der Klasse
java.lang.Thread. Der direkte Weg zu einem Thread führt
deshalb über eine Unterklasse von Thread:
Thread
MyThread
run()
In der Unterklasse muss die Methode run() überschrieben
werden. Die Methode run() beinhaltet die Programmanweisungen für den Thread. Sie ist vergleichbar mit der main()Methode des eigentlichen Java-Programmes.
K33_Threads-L, V20
© H. Diethelm
Seite 4/36
Abteilung Informatik, Fach Programmieren
FHZ
Hochschule für Technik+Architektur Luzern
K33 THREADS
Beispiel Counter1:
public class Counter1 extends Thread {
private int count = 0;
private int inc;
public Counter1(int inc) {
this.inc = inc;
}
public void run() {
for (int i=0; i<5; i++) {
try {
System.out.print(count + " ");
count += inc;
sleep((int)(1000*Math.random()));
}
catch (InterruptedException e) {
System.out.println(e);
}
}
}
}
Die Klassenmethode sleep() bewirkt, dass der aktuelle Thread
für mindestens eine bestimmte Zeit "schlafengelegt" wird; im
obigen Beispiel im Mittel für 1000 Millisekunden bzw. 1 Sekunde.
Mit anderen Worten, dem Thread wird für mindestens diese Zeit
der Prozessor nicht mehr zur Verfügung gestellt. sleep() kann
eine checked InterruptedException werfen, weshalb ein Aufruf mit try und catch erforderlich ist.
K33_Threads-L, V20
© H. Diethelm
Seite 5/36
Abteilung Informatik, Fach Programmieren
FHZ
Hochschule für Technik+Architektur Luzern
K33 THREADS
ACHTUNG: Zeiten alternativ mittels Warteschleifen aktiv zu
"verbraten" ist tabu!! In diesem Fall könnte der Prozessor während
dem Warten für andere Threads nicht mehr zur Verfügung stehen!
public class CounterDemo {
public static void main(String args[]) {
Counter1 thread1 = new Counter1(1);
Counter1 thread2 = new Counter1(-1);
thread1.start();
thread2.start();
}
}
Nach dem Instanzieren gelangt ein Thread-Objekt mit start()
zur Ausführung. Seine run()-Methode wird dann in einem
neuen Thread abgearbeitet.
ACHTUNG: Ein direkter Aufruf von run() führt zu keinem neuen
Thread; run() würde in diesem Fall nur vom aufrufenden Thread
abgearbeitet! Ein Thread endet mit dem Verlassen bzw. dem
Ende der Methode run().
Im Beispiel instanziert und startet der implizite "main-Thread" die
zwei weiteren Threads thread1 und thread2. Mit dem Ende der
Methode main() endet bereits der "main-Thread"; thread1 und
thread2 laufen noch weiter und erzeugen z.B. folgenden Output:
0 0 1 2 -1 3 4 -2 -3 -4 Process Exit...
K33_Threads-L, V20
© H. Diethelm
Seite 6/36
Abteilung Informatik, Fach Programmieren
FHZ
Hochschule für Technik+Architektur Luzern
K33 THREADS
3.2 Mittels der Schnittstelleklasse Runnable
Nicht immer ist eine Realisierung von Threads direkt via eine Unterklasse von Thread möglich, speziell weil Java die Mehrfachvererbung nicht unterstützt.
Für solche Fälle geht's indirekt via die Implementierung der
Schnittstellenklasse bzw. des Interfaces Runnable. Die Implementierung von Runnable erzwingt einzig das Vorhandensein
einer Methode run():
Runnable
run()
MyThread
run()
Ein Objekt einer beliebigen Klasse, welche Runnable implementiert und somit eine run()-Methode zu Verfügung stellt, kann nun
als Parameter dem Konstruktor eines eigentlichen ThreadObjektes übergeben werden.
K33_Threads-L, V20
© H. Diethelm
Seite 7/36
FHZ
Hochschule für Technik+Architektur Luzern
Abteilung Informatik, Fach Programmieren
K33 THREADS
Beispiel Counter2:
public class Counter2 implements Runnable {
private Thread thread;
private int count = 0;
private int inc;
public Counter2(int inc) {
this.inc = inc;
}
public void start() {
if (thread == null) {
thread = new Thread(this);
thread.start();
}
}
public void run() {
for (int i=0; i<5; i++) {
try {
System.out.print(count + " ");
count += inc;
thread.sleep((int)(1000*Math.random()));
}
catch (InterruptedException e) {
System.out.println(e);
}
}
}
}
K33_Threads-L, V20
© H. Diethelm
Seite 8/36
Abteilung Informatik, Fach Programmieren
FHZ
Hochschule für Technik+Architektur Luzern
K33 THREADS
Weil Counter2 kein Thread ist, steht z.B. nicht ohnehin eine Methode start() zur Verfügung. Diese wird deshalb explizit implementiert, wobei sie den Startbefehl an das interne Thread-Objekt
tread delegiert (vgl. Delegation):
Runnable
run()
Counter2
thread:Thread
start()
run()
Thread
1
start()
Threads der Klasse Counter2 können nun analog wie solche der
Klasse Counter1 verwendet werden:
public class CounterDemo {
static public void main(String args[]) {
Counter1 thread1 = new Counter1(1);
Counter2 thread2 = new Counter2(-1);
thread1.start();
thread2.start();
}
}
Hier nochmals ein Output:
0 0 1 2 -1 3 -2 -3 4 -4 Process Exit...
K33_Threads-L, V20
© H. Diethelm
Seite 9/36
FHZ
Hochschule für Technik+Architektur Luzern
Abteilung Informatik, Fach Programmieren
K33 THREADS
4 Animation
Threads können als aktive Elemente dafür sorgen, dass Bilder laufen lernen, z.B. "ein springender Ball".
Beispiel Bouncer1 (ein erster Versuch!):
import java.awt.*;
public class Ball1 {
private Component c; private Graphics g;
private int x=50, y=50, dx=7, dy=2, r=10;
private boolean go = true;
public Ball1(Component c, Graphics g) {
this.c = c; this.g = g;
}
public void terminate() { go = false; }
public void display() {
while (go) {
g.setColor(Color.white);
g.fillOval(x-r, y-r, 2*r, 2*r);
if ((x-r+dx) <= 0) dx = -dx;
if ((x+r+dx) >= c.getWidth()) dx = -dx;
if ((y-r+dy) <= 0) dy = -dy;
if ((y+r+dy) >= c.getHeight()) dy = -dy;
x += dx; y += dy;
g.setColor(Color.black);
g.fillOval(x-r, y-r, 2*r, 2*r);
try { Thread.sleep(50); }
catch (InterruptedException e) { }
}
c.repaint(); // clears the last ball-drawing
}
}
K33_Threads-L, V20
© H. Diethelm
Seite 10/36
Abteilung Informatik, Fach Programmieren
FHZ
Hochschule für Technik+Architektur Luzern
K33 THREADS
import java.awt.*;
import java.applet.Applet;
import java.awt.event.*;
public class Bouncer1 extends Applet
implements ActionListener {
private Button start, stop;
private Ball1 ball;
public void init() {
start = new Button("Start");
add(start);
start.addActionListener(this);
stop = new Button("Stop");
add(stop);
stop.addActionListener(this);
}
public void actionPerformed(ActionEvent e) {
if (e.getSource() == start) {
if (ball == null) {
ball = new Ball1(this, getGraphics());
ball.display();
}
}
if (e.getSource() == stop) {
if (ball != null) {
ball.terminate();
ball = null; // => frees memory
}
}
}
}
K33_Threads-L, V20
© H. Diethelm
Seite 11/36
Abteilung Informatik, Fach Programmieren
FHZ
Hochschule für Technik+Architektur Luzern
K33 THREADS
Leider kann man das Applet mit dem Stop-Button nicht mehr beenden! Der Ball springt dauernd im Fenster umher. Weshalb?
§
Der Kontrollfluss des gesamten Programms beschränkt sich einzig auf den
impliziten main-Thread.
§
Der Kontrollfluss ist zudem in der whileSchleife der Methode display() "gefangen".
§
Die Methode actionPerformed() lässt
sich somit nicht mehr aktivieren (vgl. Fokus)!
K33_Threads-L, V20
© H. Diethelm
Seite 12/36
Abteilung Informatik, Fach Programmieren
FHZ
Hochschule für Technik+Architektur Luzern
K33 THREADS
Beispiel Bouncer2:
import java.awt.*;
public class Ball2 extends Thread {
private Component c; private Graphics g;
private int x=50, y=50, dx=7, dy=2, r=10;
private boolean go = true;
public Ball2(Component c, Graphics g) {
this.c = c; this.g = g;
}
public void terminate() { go = false; }
public void run() {
while (go) {
g.setColor(Color.white);
g.fillOval(x-r, y-r, 2*r, 2*r);
if ((x-r+dx) <= 0) dx = -dx;
if ((x+r+dx) >= c.getWidth()) dx = -dx;
if ((y-r+dy) <= 0) dy = -dy;
if ((y+r+dy) >= c.getHeight()) dy = -dy;
x += dx; y += dy;
g.setColor(Color.black);
g.fillOval(x-r, y-r, 2*r, 2*r);
try { sleep(50); }
catch (InterruptedException e) { }
}
c.repaint();
}
}
K33_Threads-L, V20
© H. Diethelm
Seite 13/36
Abteilung Informatik, Fach Programmieren
FHZ
Hochschule für Technik+Architektur Luzern
K33 THREADS
import java.awt.*;
import java.applet.Applet;
import java.awt.event.*;
public class Bouncer2 extends Applet
implements ActionListener {
private Button start, stop;
private Ball2 ball;
public void init() {
start = new Button("Start");
add(start);
start.addActionListener(this);
stop = new Button("Stop");
add(stop);
stop.addActionListener(this);
}
public void actionPerformed(ActionEvent e) {
if (e.getSource() == start) {
if (ball == null) {
ball = new Ball2(this, getGraphics());
ball.start();
}
}
if (e.getSource() == stop) {
if (ball != null) {
ball.terminate();
ball = null; // => frees memory
}
}
}
}
K33_Threads-L, V20
© H. Diethelm
Seite 14/36
Abteilung Informatik, Fach Programmieren
FHZ
Hochschule für Technik+Architektur Luzern
K33 THREADS
Jetzt kann man das Applet wie gewünscht mit dem Stop-Button
beenden. Weshalb?
§
Das Programm beinhaltet 2 Threads bzw.
2 Kontrollflüsse, die "parallel" ablaufen.
§
Der ball-Thread kümmert sich um die Animation; Der main-Thread um die Interaktion mit dem Benutzer.
§
Während der ball-Thread schläft hat der
main-Thread genügend Zeit, die Benutzereingaben zu verarbeiten (vgl. quasiparallel).
Zwischen den beiden Threads findet keine eigentliche Interaktion (vgl. Synchronisation oder Kommunikation) statt, weshalb die
Programmierung einfach und unkritisch ist.
K33_Threads-L, V20
© H. Diethelm
Seite 15/36
Abteilung Informatik, Fach Programmieren
FHZ
Hochschule für Technik+Architektur Luzern
K33 THREADS
Beispiel Bouncer3:
Bouncer2 kann einfach auf beliebig viele Bälle erweitert werden.
Jeder Ball wird von einem separaten Thread animiert.
import
import
import
import
java.awt.*;
java.applet.Applet;
java.awt.event.*;
java.util.*;
public class Bouncer3 extends Applet
implements ActionListener {
private Button start, stop;
private Stack stack;
public void init() {
start = new Button("Start"); add(start);
start.addActionListener(this);
stop = new Button("Stop"); add(stop);
stop.addActionListener(this);
stack = new Stack();
}
public void actionPerformed(ActionEvent e) {
if (e.getSource() == start) {
Ball2 ball = new Ball2(this, getGraphics());
ball.start();
stack.push(ball);
}
if (e.getSource() == stop) {
if (!stack.isEmpty()) {
Ball2 ball = (Ball2) stack.pop();
ball.terminate();
}
}
}
}
K33_Threads-L, V20
© H. Diethelm
Seite 16/36
Abteilung Informatik, Fach Programmieren
FHZ
Hochschule für Technik+Architektur Luzern
K33 THREADS
4.1 Idiom für ein Animations-Applet
Unter einem Idiom versteht man ein allgemeines Lösungsmuster. So kann ein allgemeines Animations-Applet ohne Benutzerinteraktion gemäss folgendem Idiom implementiert werden:
import java.applet.Applet;
import java.awt.*;
public class Animationlet extends Applet
implements Runnable {
private Thread thread;
private int delay = 50;
private boolean go = true;
private int r = 0;
private int maxr = 100;
public void init() { }
// is called first
public void start() {
// is called second
if (thread == null) {
thread = new Thread(this);
thread.start();
}
}
K33_Threads-L, V20
© H. Diethelm
Seite 17/36
Abteilung Informatik, Fach Programmieren
FHZ
Hochschule für Technik+Architektur Luzern
K33 THREADS
public void stop() {
if (thread != null) {
go = false;
thread = null;
}
}
public void run() {
while (go) {
repaint();
try {
Thread.sleep(delay);
}
catch (InterruptedException e) {
}
}
}
public void paint(Graphics g) {
if (r > maxr) r = 0;
g.drawOval(150-r, 150-r, 2*r, 2*r);
r++;
}
}
Was animiert wohl obiges Applet?
§
Ein im Radius kontinuierlich wachsender
Kreis (r = 0...100).
§
Wird rmax (100) überschritten, so "zerplatzt" der Kreis (r à 0).
§
Das ganze "Spiel" beginnt von vorne.
K33_Threads-L, V20
© H. Diethelm
Seite 18/36
FHZ
Hochschule für Technik+Architektur Luzern
Abteilung Informatik, Fach Programmieren
K33 THREADS
5 Thread-Steuerung
5.1 Thread-Zustände
Einige Befehle ermöglichen es dem Programmierer, den Lebenszyklus von Threads zu steuern. Die eigentliche Thread-Ausführung
wird aber direkt von der JVM oder gar dem unterliegenden Betriebssystem kontrolliert.
Folgendes Zustandsdiagramm illustriert den Lebenszyklus von
Threads:
lebend
suspendiert
aktiv
sleep()
schlafend
Time out
start()
lauffähig
neu
new
wait()
wartend
notify()
notifyAll()
scheduled
yield()
join()
blockiert
eingetroffen
interrupt()
läuft
run()
beendet
tot
= null
Garbage
Collection
/throws
InterruptedException
(Anschliessend wird nur auf die wichtigsten Thread-Zustände eingegangen, die oben grau markiert sind.)
Ein Thread befindet sich immer in einem von 3 bzw. 4 Zuständen:
neu:
Nach dem Instanzieren mit new().
K33_Threads-L, V20
© H. Diethelm
Seite 19/36
Abteilung Informatik, Fach Programmieren
FHZ
Hochschule für Technik+Architektur Luzern
K33 THREADS
lebend/aktiv:
Nach dem Aufruf von start() ist der Thread lauffähig, d.h. zur
Abarbeitung bereit. Die start()-Methode ruft insbesondere die
Methode run() des Threads auf. Es ist aber nicht zwingend, dass
sie unmittelbar auf dem Prozessor zur Ausführung gelangt (vgl.
Thread-Priorität, andere lauffähige Threads)!
Der aktive Thread bewirkt mit dem Aufruf von yield(), dass ein
anderer Thread mit derselben Priorität und dem Zustand "lauffähig" den Prozessor zur Abarbeitung bekommt. Existiert kein solcher Thread, so zeigt der Aufruf keine Wirkung. Mittels yield()
kann erreicht werden, dass der Prozessor auch ohne Time-Slicing
abwechslungsweise Threads mit gleicher Priorität zur Verfügung
steht.
lebend/suspendiert:
Nach dem Aufruf von sleep(), wait()oder join() wird ein
Thread suspendiert, d.h. er ist vorläufig nicht mehr zur Ausführung bereit.
Der Aufruf von sleep() entzieht dem Thread mindestens für eine
bestimmte Zeit die Bereitschaft zur Ausführung.
Die Methoden wait(), notify() und notifyAll() sind in der
Klasse Object deklariert und beeinflussen ebenfalls die Zustände
von Threads. Der Aufruf von wait() auf ein bestimmtes Objekt
bewirkt, dass der ausführende Thread suspendiert wird. Ein weiterer Thread kann mittels notify() dasselbe Objekt benachrichtigen, so dass ein suspendierter Thread wieder in den Zustand
"lauffähig" wechselt. notifyAll() führt dazu, dass sämtliche an
diesem Objekt suspendierten Threads wieder den Zustand "lauffähig" annehmen.
K33_Threads-L, V20
© H. Diethelm
Seite 20/36
Abteilung Informatik, Fach Programmieren
FHZ
Hochschule für Technik+Architektur Luzern
K33 THREADS
tot:
Sobald die run()-Methode zu Ende abgearbeitet ist, stirbt der
Thread. Ein wiederholter Start ist nicht möglich. Der vom
Thread beanspruchte Speicher wird aber erst freigegeben, wenn
der Thread nicht mehr referenziert wird und die Garbage Collection ihre Arbeit verrichtet hat.
5.2 Thread-Prioritäten
Jeder Thread besitzt eine Priorität. Die Priorität wird mit einem Integer-Wert von 1 bis 10 repräsentiert, wobei 5 der Default-Wert ist.
Wird mit new ein neuer Thread erzeugt, so bekommt dieser automatisch die gleiche Priorität zugewiesen, die auch der Thread
besitzt, welcher new aufgerufen hat. Mit setPriority() kann
aber die Priorität nachträglich geändert werden. Sind mehrere
Threads im Zustand "lauffähig", so kommt derjenige mit der grössten Priorität zuerst zur Ausführung. Besitzen mehrere Threads die
gleiche Priorität, so wird entweder einer von ihnen zufällig ausgewählt (reines Priority Scheduling) oder alle werden abwechslungsweise ausgeführt (Priority Scheduling mit Time-Slicing).
ACHTUNG: Die korrekte Programmausführung sollte nicht von
den Thread-Prioritäten abhängig sein! Die Prioritäten sollen höchstens zur Minimierung der Laufzeit optimiert werden!
K33_Threads-L, V20
© H. Diethelm
Seite 21/36
Abteilung Informatik, Fach Programmieren
FHZ
Hochschule für Technik+Architektur Luzern
K33 THREADS
6 Kommunikation und Synchronisation
Threads sind selten voneinander unabhängig. Im Allgemeinen
kommunizieren sie miteinander oder müssen miteinander synchronisiert werden, d.h. sie haben gemeinsame "Berührungspunkte".
6.1 Motivation
Im folgenden Beispiel gibt es einen main()-Thread sowie zwei
CounterThreads ct1 und ct2. Der main()-Thread erzeugt ein
Counter-Objekt counter sowie ct1 und ct2. ct1 und ct2 greifen beide auf counter bzw. auf c zu; zählen also simultan denselben Zähler hoch.
public class CounterThread extends Thread {
private Counter c;
public static void main(String[] args) {
Counter counter = new Counter();
CounterThread ct1 = new CounterThread(counter);
CounterThread ct2 = new CounterThread(counter);
ct1.start();
ct2.start();
}
public CounterThread(Counter c) {
this.c = c;
}
public void run() {
c.count();
}
}
K33_Threads-L, V20
© H. Diethelm
Seite 22/36
FHZ
Hochschule für Technik+Architektur Luzern
Abteilung Informatik, Fach Programmieren
K33 THREADS
Die Klasse Counter realisiert den eigentlichen Zähler, wobei die
Instanzvariable i den aktuellen Zählerstand speichert. Mit Hilfe der
Methode count() soll der Zähler genau 100 Mal inkrementiert
werden. Während dem Hochzählen erfolgt jeweils die Ausgabe
des aktuellen Zählerstandes sowie ein sleep() von durchschnittlich 10 Millisekunden. Die Methode ist bewusst etwas "holprig"
implementiert.
public class Counter {
private int i = 0;
public void count() {
int limit = i + 100;
while (i++ != limit) {
System.out.println(i);
try {
Thread.sleep((int)(10*Math.random()));
}
catch (InterruptedException e) {
}
}
}
}
(Versuchen Sie als Übung, das Klassendiagramm der Anwendung
aufzuzeichnen!)
K33_Threads-L, V20
© H. Diethelm
Seite 23/36
FHZ
Hochschule für Technik+Architektur Luzern
Abteilung Informatik, Fach Programmieren
K33 THREADS
Leider verhält sich das so implementierte Programm seltsam. Das
wiederholte Starten führte beispielsweise zu folgenden Ausgaben:
a) Die beiden CounterThreads zählen nur auf 100 hoch!
b) Der Zähler zählt beliebig weit hoch!
c) Der Zähler zählt beliebig weit hoch, teilweise sogar fehlerhaft!
Was könnte wohl die Ursache für dieses nichtdeterministische
Verhalten sein?
a) ct2 wird nicht richtig berechnet. Sollte höher zählen als ct1.
b) i wird während Auswertung i++ != limit verändert
c) i wird vor Ausgabe System.out.println(i) verändert
K33_Threads-L, V20
© H. Diethelm
Seite 24/36
Abteilung Informatik, Fach Programmieren
FHZ
Hochschule für Technik+Architektur Luzern
K33 THREADS
Die Problematik liegt darin, dass beide CounterThreads auf demselben Counter bzw. derselben Instanzvariablen i operieren. Die
Instanzvariable i stellt dabei einen gemeinsamen Speicherbereich für beide Threads dar (vgl. Shared Memory):
Es ist nicht eindeutig, wie die beiden Threads zeitlich auf die gemeinsame Variable i zugreifen und diese manipulieren!
6.2 Gegenseitiger Ausschluss (mutual exclusion = mutex)
Obige Problematik taucht immer dann auf, wenn mehrere
Threads auf gemeinsame Betriebsmittel (vgl. Prozesskontext)
zugreifen wollen; die Mittel aber verlangen, dass nur 1 Thread zugreift! Beispiele dafür sind:
Speicher, Filesystem, Drucker, Netzwerk
K33_Threads-L, V20
© H. Diethelm
Seite 25/36
Abteilung Informatik, Fach Programmieren
FHZ
Hochschule für Technik+Architektur Luzern
K33 THREADS
Folgendes Beispiel illustriert die Problematik anschaulicher. Wir
nehmen dazu an, zu Beginn sei a = 0:
Die CR (Critical Region) kann von den beiden Threads verschieden durchlaufen werden (vgl. Ausführung auf Stufe Byte-Code):
a)
Die Variable a besitzt nun welchen Wert?
b)
Welchen Wert besitzt wiederum die Variable a?
K33_Threads-L, V20
© H. Diethelm
Seite 26/36
Abteilung Informatik, Fach Programmieren
FHZ
Hochschule für Technik+Architektur Luzern
K33 THREADS
Der Programmzustand ist also davon abhängig, wie die
Threads abgearbeitet werden (Race Condition)!!
Bemerkung: LOAD, ADD und STORE sind sogenannte atomare
Anweisungen, die bei ihrer Ausführung nicht unterbrochen werden können.
Gefragt sind also Lösungen, die den gegenseitigen Ausschluss bei
einer CR garantieren.
6.3 Das Monitor-Konzept bei Java
Java realisiert den gegenseitigen Ausschluss mit Hilfe des Monitorkonzeptes (C.A.R. Hoare, P. Hansen). Ein Monitor überwacht
eine oder mehrere CRs und sorgt dafür, dass diese nicht von
mehreren Threads gleichzeitig benutzt werden. CRs sind Methoden oder seltener Anweisungsblöcke, die mit dem Schlüsselwort synchronized gekennzeichnet sind.
Für jedes Objekt, das als CRs gekennzeichnete Methoden
enthält, legt Java zur Laufzeit einen Monitor an. Betritt ein
Thread während der Abarbeitung eine CR, so hat kein anderer
Thread mehr Zugriff auf synchronisierte Methoden desselben
Objektes. Er wird beim Versuch des Zugriffs vielmehr blockiert, bis der frühere Thread die CR wieder freigibt.
Als CRs gekennzeichnete Anweisungsblöcke können grundsätzlich von einem Monitor eines beliebigen Objektes kontrolliert
werden. Deshalb folgt dem Schlüsselwort synchronized eine
Referenzvariable, die zum entsprechenden Objekt verweist.
Ein Monitor ist also dafür besorgt, dass in seinem Zuständigkeitsbereich nur eine CR aufs Mal betreten werden kann. Eine
CR repräsentiert sozusagen 1 "atomare Anweisung". Nicht synchronisierte Methoden und Anweisungsblöcke können selbstverständlich nebeneinander von mehreren Threads benutzt werden.
K33_Threads-L, V20
© H. Diethelm
Seite 27/36
FHZ
Hochschule für Technik+Architektur Luzern
Abteilung Informatik, Fach Programmieren
K33 THREADS
Auf unser Programmbeispiel übertragen, muss einzig die Methode
count() mit dem Modifizierer synchronized ergänzt werden:
public class Counter {
private int i = 0;
public synchronized void count() {
int limit = i + 100;
while (i++ != limit) {
System.out.println(i);
try {
Thread.sleep((int)(10*Math.random()));
}
catch (InterruptedException e) {
}
}
}
}
Die Threads ct1 und ct2 kommen sich jetzt gegenseitig nicht
mehr ins Gehege und liefern einen deterministischen Output:
Allerdings wird bei dieser Lösung zuerst ct1 vollständig abgearbeitet und erst anschliessend ct2. Die beiden Threads laufen also
nicht "verzahnt" ab.
K33_Threads-L, V20
© H. Diethelm
Seite 28/36
FHZ
Hochschule für Technik+Architektur Luzern
Abteilung Informatik, Fach Programmieren
K33 THREADS
Folgende Lösung ermöglicht, dass während der eine Thread
schläft, der andere (falls er nicht auch schläft) weiterzählen kann:
public class Counter {
private int i = 1;
public void count() {
for (int m=0; m<100; m++) {
synchronized(this) {
System.out.println(i);
i = i+1;
}
try {
Thread.sleep((int)(10*Math.random()));
}
catch (InterruptedException e) {
}
}
}
}
Oben ist nur ein Anweisungsblock mit synchronized gekennzeichnet. Das nachgestellte (this) legt fest, dass der Anweisungsblock vom Monitor des aktuellen Objektes kontrolliert wird.
ct1 und ct2 generieren nun parallel den Output:
K33_Threads-L, V20
© H. Diethelm
Seite 29/36
Abteilung Informatik, Fach Programmieren
FHZ
Hochschule für Technik+Architektur Luzern
K33 THREADS
6.4 Beispiel "Produzent-Konsument"
Im bisherigen Beispiel haben zwei Threads via Shared Memory
miteinander kommuniziert. Die beiden Threads liefen aber unabhängig voneinander ab; eine durch die Problemstellung bedingte
Synchronisation war nicht erforderlich.
Bei folgendem Beispiel ist dies anders: Ein Produzent produziert
Daten und übergibt diese einer Warteschlange. Die Warteschlange
kann dabei nur eine begrenzte Menge von Daten aufnehmen. Umgekehrt entnimmt ein Konsument die Daten wieder der Warteschlange, um sie anschliessend zu konsumieren.
Analog dem letzten Beispiel haben wir es bei der Warteschlange
ebenfalls mit Shared Memory bzw. einer CR zu tun. Über die
Warteschlange können der Producer- und Consumer-Thread miteinander kommunizieren. Allerdings ist hier zusätzlich eine eigentliche Synchronisation erforderlich. So kann der ProducerThread an die Warteschlange nur Daten übergeben, falls die Warteschlange nicht voll ist. Umgekehrt kann der Consumer-Thread
nur Daten entnehmen, wenn die Warteschlange nicht leer ist. Die
Zugriffsmethoden put() und get() der Warteschlange müssen
dies gegenseitig entsprechend signalisieren (vgl. wait() und notify()).
Das beschriebene Produzent-Konsument Beispiel hat stellvertretenden Charakter und kommt häufig in ähnlicher Form vor.
K33_Threads-L, V20
© H. Diethelm
Seite 30/36
FHZ
Hochschule für Technik+Architektur Luzern
Abteilung Informatik, Fach Programmieren
K33 THREADS
Produzent:
public class Producer extends Thread {
private BoundedQueue queue;
private int n;
public Producer(BoundedQueue queue, int n) {
this.queue = queue;
this.n = n;
}
public void run() {
for (int i=0; i<n; i++) {
queue.put(new Integer(i));
System.out.println("produce: " + i);
try {
sleep((int)(500*Math.random()));
}
catch (InterruptedException e) {
}
}
}
}
Ein Producer-Thread produziert also insgesamt n Integer-Objekte
und legt diese in der Warteschlange queue ab. Dazwischen legt er
sich durchschnittlich für mindestens eine halbe Sekunde schlafen.
K33_Threads-L, V20
© H. Diethelm
Seite 31/36
FHZ
Hochschule für Technik+Architektur Luzern
Abteilung Informatik, Fach Programmieren
K33 THREADS
Konsument:
public class Consumer extends Thread {
private BoundedQueue queue;
private int n;
public Consumer(BoundedQueue queue, int n) {
this.queue = queue;
this.n = n;
}
public void run() {
for (int i=0; i<n; i++) {
Object o = queue.get();
if (o != null) {
System.out.println("
consume: " + o);
}
try {
sleep((int)(1000*Math.random()));
}
catch (InterruptedException e) {
}
}
}
}
Ein Consumer-Thread entnimmt der Warteschlange queue insgesamt n Objekte und druckt diese auf der Konsole aus. Dazwischen
legt er sich durchschnittlich für mindestens eine Sekunde schlafen.
K33_Threads-L, V20
© H. Diethelm
Seite 32/36
Abteilung Informatik, Fach Programmieren
FHZ
Hochschule für Technik+Architektur Luzern
K33 THREADS
Warteschlange, Hauptprogramm:
public class BoundedQueue {
private
private
private
private
private
Object[] array;
int front = 0;
int back = -1;
int size = 0;
int count = 0;
public BoundedQueue(int size) {
if (size > 0) {
this.size = size;
array = new Object[size];
back = size - 1;
}
}
synchronized public boolean isEmpty() {
return (count == 0);
}
synchronized public boolean isFull() {
return (count == size);
}
synchronized public int getCount() {
return count;
}
synchronized public void put(Object o) {
if (o != null) {
try {
while (isFull()) wait();
back++;
K33_Threads-L, V20
© H. Diethelm
Seite 33/36
FHZ
Hochschule für Technik+Architektur Luzern
Abteilung Informatik, Fach Programmieren
K33 THREADS
if (back >= size) back = 0;
array[back] = o;
count++;
notify();
}
catch (InterruptedException e) {
}
}
}
synchronized public Object get() {
Object o = null;
try {
while (isEmpty()) wait();
o = array[front];
array[front] = null;
front++;
if (front >= size) front = 0;
count--;
notify();
}
catch (InterruptedException e) {
}
return o;
}
public static void main(String args[]) {
BoundedQueue queue = new BoundedQueue(5);
new Producer(queue, 10).start();
new Consumer(queue, 10).start();
}
}
K33_Threads-L, V20
© H. Diethelm
Seite 34/36
Abteilung Informatik, Fach Programmieren
FHZ
Hochschule für Technik+Architektur Luzern
K33 THREADS
Sämtliche Zugriffsmethoden sind synchronisiert, so dass
gleichzeitig nur ein Thread Zugriff auf den gemeinsamen Speicherbereich bzw. auf die CR hat.
Die Methode put() kann ein beliebiges Objekt in die Warteschlange bzw. im Pufferspeicher ablegen. Falls die Warteschlange voll ist, ist ein Ablegen nicht möglich bzw. der entsprechende
Thread muss warten.
Der Aufruf von wait() bewirkt, dass der aktuelle Thread in den
Zustand "wartend" übergeht. wait() kann nur innerhalb eines
synchronisierten Abschnittes für dasjenige Objekt aufgerufen
werden (wait() ist eine Methode der Klasse Object!), dessen
Monitor diesen Abschnitt kontrolliert. Obwohl der besagte
Thread den synchronisierten Abschnitt bzw. die CR noch nicht zu
Ende abgearbeitet hat, gibt in diesem Falle der Monitor seine von
ihm kontrollierten CRs für andere Threads wieder frei!
Erst ein analoger Aufruf von notify() (siehe Methode get())
bewirkt, dass unser Thread oder einer, der bereits früher an derselben Stelle suspendiert wurde, wieder in den Zustand "lauffähig" wechselt. (Mit notifyAll() würden alle an dieser Stelle
suspendierten Threads wieder den Zustand "lauffähig" annehmen.)
Sobald er effektiv zur Ausführung gelangt, wird er dort fortsetzen,
wo er einst unterbrochen wurde.
In unserem Fall wird er in der while-Schleife nochmals isFull()
aufrufen. Dies ist wichtig, denn es könnte ja sein, dass in der Zwischenzeit andere Threads die Warteschlange bereits wieder gefüllt
haben und ein erneutes Warten angesagt ist!
Der Aufruf von notify() am "Ende" der Methode put() signalisiert umgekehrt solchen Threads, die innerhalb der Methode
get() suspendiert wurden (weil die Warteschlange leer war),
dass jetzt mindestens wieder ein Objekt in der Warteschlange drin
verfügbar ist.
K33_Threads-L, V20
© H. Diethelm
Seite 35/36
Abteilung Informatik, Fach Programmieren
FHZ
Hochschule für Technik+Architektur Luzern
K33 THREADS
Der Output zeigt, wie Produzent und Konsument zusammenarbeiten und wie letzterer schliesslich alle einst produzierten Daten auf
der Konsole ausgibt:
Die Warteschlange hat gemäss Instanzierung für 5 Objekte Platz.
Tatsächlich sind zu jedem Zeitpunkt nicht mehr wie 5 IntegerObjekte im Puffer!
K33_Threads-L, V20
© H. Diethelm
Seite 36/36
Herunterladen