4 Einführung in Java Threads

Werbung
Praktikum zur Lehrveranstaltung Betriebssysteme
Seite 4 - 1
4 Einführung in Java Threads
Die meisten Betriebssysteme erlauben, dass mehrere Programme auf Multiprozessorsystemen
oder Multicore-Prozessorsystemen gleichzeitig (also zeitlich echt parallel) und auf Einprozessorsystemen quasi-gleichzeitig (zeitlich ineinander verschachtelt, timesharing) ausgeführt werden.
Bei einem Multiprozessor- oder Multicore-Prozessorsystem werden die Programme auf den einzelnen Prozessoren oder Prozessorkernen im Allgemeinen auch noch zeitlich ineinander verschachtelt, um eine möglichst hohe Leistungsfähigkeit und Fairness zu erreichen. Diese Konzepte wurden auch auf eine Parallelität innerhalb eines Prozesses erweitert, z. B. Coroutinen in den
Programmiersprachen „Simula“ (process) oder „ADA“ (task). Im Betriebssystem Mach wurden
in der zweiten Hälfte der 1980-er Jahre C-Threads eingeführt und untersucht, die die Parallelität
innerhalb eines Programmes auch auf Betriebssystemebene unterstützen. Die bekanntesten
Schnittstellen zur Thread-Programmierung in der Programmiersprache C sind die standardisierten „POSIX-Threads“ (pthreads), die von den meisten Betriebssystemen unterstützt werden und
die „Win32-Threads“ der Firma Microsoft für ihre Windows-Betriebssysteme. Die Programmiersprache Java stellt ebenfalls eine Thread-Schnittstelle zur Verfügung, die, wie Java selbst, prozessorarchitektur- und betriebssystemübergreifend ist. Java-Threads funktionieren auch dann,
wenn das Betriebssystem keine Threads unterstützt, da die entsprechende Funktionalität in
diesem Fall in der virtuellen Maschine von Java zur Verfügung gestellt wird.
Warum brauchen wir überhaupt Threads? Schauen wir uns einfach ein kleines Programm an, in
dem ein Ball in einer Zeichenfläche bewegt wird. Wenn der Ball an eine Kante stößt, wird er dort
reflektiert und setzt seine Bahn in einer anderen Richtung fort (übersetzen und starten Sie das
Programm MoveBallMain.java im Verzeichnis prog/Thread/MoveBall des Programmarchivs;
im Folgenden befinden sich alle Verzeichnisse unterhalb des Verzeichnisses prog/Thread).
Wenn Sie mehrfach auf „Start blue ball“ klicken, passiert erst einmal nichts. Wenn der erste Ball
seine Bahn beendet hat, startet der zweite Ball usw. Damit Sie sehen, dass ein neuer Ball gestartet wurde, bleibt die letzte Position des „alten“ Balls auf dem Bildschirm erhalten. Wenn Sie auf
Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
Praktikum zur Lehrveranstaltung Betriebssysteme
Seite 4 - 2
„Close“ klicken, passiert zunächst wieder nichts. Das Fenster wird erst dann geschlossen, wenn
der letzte Ball seine Bahn beendet hat. Eigentlich sollten mehrere Bälle parallel über die Zeichenfläche „fliegen“ und das Fenster auch sofort geschlossen werden, wenn wir auf „Close“ klicken.
Diese Parallelität können wir mit Hilfe von Java-Threads implementieren.
Ein Thread kann zum Beispiel dadurch erzeugt werden, dass die Klasse Thread aus java.lang
erweitert (public class Thread extends Object implements Runnable) oder die Schnittstelle
Runnable aus java.lang implementiert (public interface Runnable) wird. Falls die Klasse, die als
Thread laufen soll, bereits von einer anderen Klasse erbt, muss „Runnable“ implementiert werden, da Java keine Mehrfachvererbung erlaubt. In jedem Fall muss die Methode „run ()“ überschrieben werden, die dann das eigentliche Programm des Threads enthält. Die Methode „run ()“
darf niemals direkt aufgerufen werden, da die Methode dann nicht parallel zur aufrufenden
Methode ausgeführt wird, sondern sequentiell innerhalb der Methode abläuft (quasi wie ein
Unterprogrammaufruf). Ein Thread wird immer mit der Methode „start ()“ gestartet, die die
Thread-Umgebung initialisiert, danach die weitere Ausführung an die Methode „run ()“ überträgt
und sich dann beendet, sodass der neue Thread jetzt parallel zum Erzeuger des Threads abläuft.
Das folgende Programm im Verzeichnis HelloThread des Programmarchivs erweitert
beispielsweise die Klasse Thread.
/* Program creates a thread which prints a message on the screen.
* ...
* File: HelloOneMain.java
Author: S. Gross
*/
public class HelloOneMain
{
public static void main (String args[])
{
HelloThread hello = new HelloThread ();
hello.start ();
}
}
Im Hauptprogramm wird nur ein neues Thread-Objekt erzeugt und gestartet. Danach wartet das
Hauptprogramm automatisch darauf, dass der Thread endet, bevor es sich selbst beendet.
/* This thread displays a message on the screen.
* ...
* File: HelloThread.java
Author: S. Gross
*/
public class HelloThread extends Thread
{
private int threadID;
/* constructors
public HelloThread ()
{
this.threadID = 0;
setName ("HelloThread-" + Integer.toString (threadID));
}
public HelloThread (int i)
{
this.threadID = i;
setName ("HelloThread-" + Integer.toString (threadID));
}
Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
*/
Praktikum zur Lehrveranstaltung Betriebssysteme
Seite 4 - 3
/* implementation of method "run"
public void run ()
{
System.out.println (this.toString () + ": Hello!");
}
*/
/* overloading "toString ()"
public String toString ()
{
return getName ();
}
*/
}
Im Konstruktor der Klasse HelloThread wird ein Name für den Thread mit Hilfe der Methode
„setName ()“ der Klasse Thread erzeugt. Wenn der Aufruf dieser Methode fehlt, wird vom
System automatisch ein globaler Name der Form „Thread-n“ vergeben, wobei „n“ eine ganze
Zahl ist. Außerdem wird in der Klasse die Methode „toString ()“ überschrieben, sodass nur noch
der Name des Threads ausgegeben wird (standardmäßig würde die Methode den Namen, die
Priorität und die Gruppe des Threads ausgeben). Wie Sie der Methode „run ()“ entnehmen
können, gibt der Thread nur seinen Namen und „Hello!“ aus. Die Dateien „HelloTwoMain.java“
und „HelloThreeMain.java“ zeigen, wie Sie mehrere Threads erzeugen können.
/* Program creates some threads which print messages on the screen.
* ...
* File: HelloThreeMain.java
Author: S. Gross
*/
public class HelloThreeMain
{
public static void main (String args[])
{
final int NUMBER_OF_THREADS = 4;
HelloThread hello[] = new HelloThread [NUMBER_OF_THREADS];
for (int i = 0; i < NUMBER_OF_THREADS; ++i)
{
hello[i] = new HelloThread (i);
hello[i].start ();
}
}
}
Sie sollten sich immer die Identifikatoren der Threads merken, damit Sie ggf. spezielle Methoden für einen Thread aufrufen können (z. B. „interrupt ()“, um einen Thread zu beenden). Sie
müssen auch immer bestimmte Werte als Konstante definieren (Programmierrichtlinien!), damit
sie ggf. an einer zentralen Stelle geändert werden können und nicht an mehreren Stellen geändert
werden müssen (z. B. NUMBER_OF_THREADS). Das Erzeugen und Starten eines Threads kann
auch in einer Anweisung erfolgen. Im Programm „HelloFourMain.java“ wird die start-Methode
direkt an die Zuweisung angefügt ((hello[i] = new HelloThread (i)).start ();) und im Programm
„HelloFiveMain.java“ wird ein Thread erzeugt, dessen Identifikator nicht gespeichert wird ((new
HelloThread (i)).start ();), sodass mit ihm später nicht kommuniziert werden kann.
Im Verzeichnis HelloRunnable finden Sie dieselben Programme in der Version, die die Schnittstelle Runnable implementiert. In diesem Fall benötigen Sie einen Thread-Konstruktor, der aus
Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
Praktikum zur Lehrveranstaltung Betriebssysteme
Seite 4 - 4
einem Runnable einen Thread erzeugt. Die Klasse Thread stellt die folgenden Konstruktoren zur
Verfügung.
1)
Thread (ThreadGroup group, Runnable target, String name)
Erzeugt einen neuen Thread, der target ausführt, den Namen name hat und zur Gruppe
group gehört. Falls die Gruppe null ist, ist der Thread im Allgemeinen in derselben Gruppe
wie der erzeugende Thread (hängt davon ab, ob es einen sogenannten Security Manager
gibt). Falls target den Wert null hat, muss die Methode „run ()“ in diesem Thread implementiert worden sein, damit der Thread etwas macht (class ... implements Runnable {...}).
Falls target einen Wert ungleich null hat, wird die Methode „run ()“ von target aufgerufen.
Die Priorität des neuen Threads entspricht der Priorität des erzeugenden Threads (kann mit
der Methode „setPriority ()“ der Klasse Thread geändert werden).
2)
Thread ()
Entspricht Thread (null, null, gname), wobei gname ein neu erzeugter Name der Form
Thread-n ist (n ist eine ganze Zahl).
3)
Thread (Runnable target)
Entspricht Thread (null, target, gname), wobei gname ein neu erzeugter Name der Form
Thread-n ist (n ist eine ganze Zahl).
4)
Thread (Runnable target, String name)
Entspricht Thread (null, target, name).
5)
Thread (String name)
Entspricht Thread (null, null, name).
6)
Thread (ThreadGroup group, Runnable target)
Entspricht Thread (group, target, gname), wobei gname ein neu erzeugter Name der Form
Thread-n ist (n ist eine ganze Zahl).
7)
Thread (ThreadGroup group, String name)
Entspricht Thread (group, null, name).
8)
Thread (ThreadGroup group, Runnable target, String name, long stackSize)
Verhält sich im Wesentlichen genauso wie „Thread (ThreadGroup group, Runnable target,
String name)“. Sie können allerdings zusätzlich die Größe des Kellerspeichers angegeben.
Falls die Größe Null ist, entspricht der Aufruf exakt dem anderen Aufruf. Die Implementierung dieses Konstruktors ist abhängig von der Plattform. Unter Umständen wird der
Parameter von der Implementierung überhaupt nicht beachtet! Sie sollten diesen Konstruktor nicht verwenden.
Im folgenden Hauptprogramm „HelloOneMain.java“ im Verzeichnis HelloRunnable wird
zuerst ein Runnable-Objekt erzeugt, dem man auch wieder einen Namen geben kann. Danach
kann mit dem Runnable-Objekt ein Thread erzeugt und gestartet werden.
Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
Praktikum zur Lehrveranstaltung Betriebssysteme
Seite 4 - 5
/* Program creates a thread which prints a message on the screen.
* This version uses the interface Runnable.
* ...
* File: HelloOneMain.java
Author: S. Gross
*/
public class HelloOneMain
{
public static void main (String args[])
{
String threadName = "HelloRunnable";
/* create runnable object
HelloRunnable helloRun = new HelloRunnable (threadName);
/* create thread with constructor Thread (target)
Thread hello = new Thread (helloRun);
hello.start ();
*/
*/
}
}
/* This thread displays a message on the screen.
* ...
* File: HelloRunnable.java
Author: S. Gross
*/
public class HelloRunnable implements Runnable
{
private int
threadID;
private String name;
/* constructors
public HelloRunnable ()
{
this.threadID = 0;
this.name
= "HelloRunnable-" + Integer.toString (threadID);
}
*/
public HelloRunnable (String name)
{
this.threadID = 0;
this.name
= name + "-" + Integer.toString (threadID);
}
public HelloRunnable (int i, String name)
{
this.threadID = i;
this.name
= name + "-" + Integer.toString (threadID);
}
/* implementation of method "run"
public void run ()
{
System.out.println (name + ": Hello!");
}
*/
}
Die Programme „HelloTwoMain.java“, „HelloThreeMain.java“ und „HelloFourMain.java“ zeigen wieder die verschiedenen Möglichkeiten, wie der Thread in „Kurzform” erzeugt werden
kann. Manchmal kann es sinnvoll sein, dass sich ein Thread selbst startet. Die entsprechenden
Programme befinden sich im Verzeichnis HelloThreadAutoStart.
Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
Praktikum zur Lehrveranstaltung Betriebssysteme
Seite 4 - 6
/* Program creates some threads which start themselves.
* ...
* File: HelloAutoStartMain.java
Author: S. Gross
*/
public class HelloAutoStartMain
{
public static void main (String args[])
{
final int NUMBER_OF_THREADS = 4;
String threadName
= "HelloAutoStart";
HelloAutoStart hello[] = new HelloAutoStart [NUMBER_OF_THREADS];
System.out.println ("\n" + Thread.currentThread ().getName () +
": create " + NUMBER_OF_THREADS +
" threads which start themselves.");
for (int i = 0; i < NUMBER_OF_THREADS; ++i)
{
/* create thread which starts itself
hello[i] = new HelloAutoStart (i, threadName);
}
*/
}
}
/* This thread starts itself in its constructor.
* ...
* File: HelloAutoStart.java
Author: S. Gross
*/
public class HelloAutoStart implements Runnable
{
private int
threadID;
private String name;
/* constructors
...
public HelloAutoStart (int i, String name)
{
this.threadID = i;
this.name
= name + "-" + Integer.toString (threadID);
/* start thread with constructor "Thread (Runnable target)"
(new Thread (this)).start ();
}
*/
/* implementation of method "run"
public void run ()
{
System.out.println (name + ": Hello!");
}
*/
*/
}
Obwohl „main ()“ ein Thread ist, können die Methoden der Klasse „Thread“ nicht benutzt
werden, da die Thread-Referenz auf „main“ fehlt. Dieser Mangel kann durch die Methode
„currentThread ()“ behoben werden, wie Sie dem obigen Beispiel entnehmen können. Da die
Bestimmung der Thread-Referenz relativ „teuer“ ist, sollte das Ergebnis zwischengespeichert
werden, wenn es häufiger benötigt wird. Nachdem Sie jetzt Threads erzeugen können, müssen
Sie noch wissen, wie Sie einen Thread beenden können, der z. B. in einer Endlosschleife läuft.
Im Allgemeinen wird für den zu beendenden Thread die Methode „interrupt ()“ aufgerufen. Der
Thread soll sich dann selbst so schnell wie möglich beenden, nachdem er ggf. alle privaten
Betriebsmittel freigegeben hat. Die entsprechenden Beispielprogramme befinden sich im
Verzeichnis HelloTerminate.
Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
Praktikum zur Lehrveranstaltung Betriebssysteme
Seite 4 - 7
/* Program creates a thread which prints a message on the screen.
* This version uses "interrupt ()" and "Thread.interrupted ()"
* ...
* File: HelloOneMain.java
Author: S. Gross
*/
public class HelloOneMain
{
public static void main (String args[])
{
final long WAIT_TIME
= 5000;
/* 5 seconds
final int NUMBER_OF_THREADS = 4;
*/
HelloOneThread hello[] = new HelloOneThread [NUMBER_OF_THREADS];
/* create all threads
for (int i = 0; i < NUMBER_OF_THREADS; ++i)
{
hello[i] = new HelloOneThread (i);
hello[i].start ();
}
*/
/* wait some time before terminating all threads
try
{
Thread.sleep (WAIT_TIME);
} catch (InterruptedException e)
{
System.err.println ("HelloOneMain: received unexpected " +
"InterruptedException.");
}
*/
/* Terminate all threads.
for (int i = 0; i < NUMBER_OF_THREADS; ++i)
{
System.out.println ("HelloOneMain: I stop thread '" +
hello[i].getName () + "' via " +
"'interrupt ()'.");
(hello[i]).interrupt ();
}
*/
/* Join all terminating threads
for (int i = 0; i < NUMBER_OF_THREADS; ++i)
{
String thrName = hello[i].getName ();
*/
try
{
hello[i].join ();
System.out.println ("HelloOneMain: '" + thrName +
"' terminated.");
} catch (InterruptedException e)
{
System.err.println ("HelloOneMain: received unexpected " +
"InterruptedException while joining "+
"'" + thrName + "'.");
}
}
}
}
Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
Praktikum zur Lehrveranstaltung Betriebssysteme
Seite 4 - 8
Im obigen Programm sehen Sie, dass Sie den Thread-Identifikator benötigen, wenn Sie einen
Thread beenden wollen oder den speziellen Namen eines Threads benutzen wollen. Weiterhin
sehen Sie, wie Sie mit der Methode „join ()“ auf das Ende eines Threads warten können, bevor
andere Aktionen erfolgen (z. B. Dateien parallel aus dem Internet laden und deren Bearbeitung
starten, wenn alle Dateien vorhanden sind).
/* This thread displays a message on the screen.
* This version uses "interrupt ()" and "Thread.interrupted ()"
* ...
* File: HelloOneThread.java
Author: S. Gross
* Date: 17.09.2007
*/
public class HelloOneThread extends Thread
{
private int threadID;
/* constructors
public HelloOneThread ()
{
this.threadID = 0;
setName ("HelloThread-" + Integer.toString (threadID));
}
*/
public HelloOneThread (int i)
{
this.threadID = i;
setName ("HelloThread-" + Integer.toString (threadID));
}
/* implementation of method "run"
public void run ()
{
final int MAX_SLEEP_TIME = 2000;
*/
/* max. 2 s
for (int i = 0; !Thread.interrupted (); ++i)
{
System.out.println (this.toString () + ": Hello!");
try
/* simulate work
{
Thread.sleep ((int) (Math.random () * MAX_SLEEP_TIME));
} catch (InterruptedException e)
{
System.err.println (getName () + ": OK, I will " +
"terminate now.");
/* The catch-statement has cleared the interrupt-status-flag
* so that we must call "interrupt ()" once more (otherwise
* the progam will continue to run).
*/
interrupt ();
}
}
*/
*/
}
/* overloading "toString ()"
public String toString ()
{
return getName ();
}
}
Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
*/
Praktikum zur Lehrveranstaltung Betriebssysteme
Seite 4 - 9
In der zweiten Version dieses Programms stellt der „Thread“ selbst eine spezielle Methode zum
Stoppen des Threads zur Verfügung.
/* Program creates a thread which prints a message on the screen.
* This version implements its own method "stopThread ()".
* ...
* File: HelloTwoMain.java
Author: S. Gross
*/
public class HelloTwoMain
{
public static void main (String args[])
{
...
/* Terminate all threads.
for (int i = 0; i < NUMBER_OF_THREADS; ++i)
{
System.out.println ("HelloTwoMain: I stop thread '" +
hello[i].getName () + "' via " +
"'stopThread ()'.");
(hello[i]).stopThread ();
}
...
}
}
/* This thread displays a message on the screen.
* ...
* File: HelloTwoThread.java
Author: S. Gross
*/
public class HelloTwoThread extends Thread
{
private boolean isNotInterrupted;
/* interrupt received?
...
/* implementation of method "run"
public void run ()
{
...
isNotInterrupted = true;
for (int i = 0; isNotInterrupted; ++i)
{
try
/* simulate it
{
Thread.sleep ((int) (Math.random () * MAX_SLEEP_TIME));
} catch (InterruptedException e)
{
...
isNotInterrupted = false;
/* terminate loop
}
}
}
/* allow termination of thread
public void stopThread ()
{
try
{
isNotInterrupted = false;
this.interrupt ();
} catch (SecurityException e) {...}
}
}
Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
*/
*/
*/
*/
*/
*/
Praktikum zur Lehrveranstaltung Betriebssysteme
Seite 4 - 10
Die dritte Version des Programms benutzt eine Mischung aus den beiden ersten Versionen und
die vierte Version packt den gesamten Code der Methode „run ()“ in einen „try-catch“-Block.
Alternativ kann das Hauptprogramm auch mit „System.exit (0)“ die virtuelle Maschine von Java
und damit alle Threads beenden (wird z. B. später in „SyncBlock/SyncBlockOneMain.java“
benutzt). Mit den bisher erworbenen Kenntnissen kann das kleine Grafikprogramm um Threads
erweitert werden. Übersetzen und starten Sie das Programm MoveBallMain.java im Verzeichnis
MoveBallThreads. Die Oberfläche verhält sich jetzt so, wie sie sich verhalten soll, d. h., jeder
Klick wird sofort bearbeitet.
Manchmal ist es sinnvoll, Threads in Gruppen zusammenzufassen, damit eine Aktion für alle
Threads der Gruppe ausgeführt werden kann (z. B. beenden von Threads). Hierfür gibt es in Java
die Klasse ThreadGroup mit entsprechenden Methoden. Man kann das obige Programm zum
Beispiel um rote Bälle erweitern. Alle blauen Bälle gehören in eine Thread-Gruppe und alle
roten in eine andere. In diesem Fall könnte man alle Threads einer Gruppe auf einmal beenden.
Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
Praktikum zur Lehrveranstaltung Betriebssysteme
Seite 4 - 11
Ein Ball kann nur entfernt werden, wenn sein Thread noch aktiv ist, d. h., dass sich der Ball noch
bewegt. Bälle, die sich nicht mehr bewegen, gehören eigentlich nicht mehr auf die Zeichenfläche,
da es keinen Thread gibt, der sie bei einer Änderung der Fenstergröße neu zeichnet. Ich habe den
„letzten“ Ball nur auf der Zeichenfläche gelassen, damit man die letzte Aktion des Threads sehen
kann. Bei einer Größenänderung des Fensters würden diese Bälle automatisch gelöscht werden.
Der folgende Codeausschnitt zeigt das Erzeugen und Beenden der Threads (die Datei befindet
sich im Verzeichnis MoveBallTreadGroup).
/* ...
* File: WindowWithButtons.java
Author: S. Gross
*/
...
public class WindowWithButtons extends JFrame
{
...
private static int thrRedID = 0;
/* thread number
private static int thrBlueID = 0;
private ThreadGroup blueBalls = new ThreadGroup ("blue balls"),
redBalls = new ThreadGroup ("red balls");
...
private class StartBlueBallButtonListener implements ActionListener
{
public void actionPerformed (ActionEvent event)
{
Thread ball =
new Thread (blueBalls,
new MoveBall (canvas, Color.blue),
"MoveBlueBallThread-" +
Integer.toString (thrBlueID++));
ball.start ();
/* let the ball bounce around
}
}
*/
*/
private class RemoveBlueBallButtonListener implements ActionListener
{
public void actionPerformed (ActionEvent event)
{
blueBalls.interrupt ();
}
}
...
}
Jeder Thread hat eine Priorität. Manchmal ist es sinnvoll, die Priorität eines Threads zu ändern,
da nicht alle Java-Implementierungen ein Zeitscheibenverfahren (round-robin) unterstützen und
ein rechenintensiver Thread dadurch alle anderen Threads in ihrer Arbeit behindern kann (im
Extremfall arbeitet nur der eine Thread). Der Grund hierfür liegt darin, dass der Scheduler u. U.
nur dann aktiv werden kann, wenn ein Thread eine Thread-Methode aufruft (was ein rechenintensiver Thread u. U. nicht macht). Falls mehrere Threads eine gleich-hohe Priorität haben und
ein Zeitscheibenverfahren zur Verfügung steht, erhalten alle Threads zyklisch nacheinander je
eine Zeitscheibe. Falls die Threads verschiedene Prioritäten haben, erhält der Thread mit der
höchsten Priorität den Prozessor, sodass Threads mit geringer Priorität u. U. beliebig lange
warten müssen (starvation). Je höher die Priorität eines Threads ist, desto seltener sollte er in der
Regel den Prozessor belegen, damit alle Threads arbeiten können. Wenn viele Threads dieselbe
Priorität haben und ein Thread den Prozessor monopolisiert, kann man sich die Prioritäten aber
Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
Praktikum zur Lehrveranstaltung Betriebssysteme
Seite 4 - 12
auch zu Nutze machen, um das Scheduling einer Java-Implementierung ggf. zu „verbessern“,
indem man einen Thread hoher Priorität immer kurz aufweckt, um dem Scheduler die Gelegenheit zu geben, einen anderen Thread auf den Prozessor zu bringen. Die Klasse Thread stellt hierfür die Konstanten „MAX_PRIORITY“, „MIN_PRIORITY“ und „NORM_PRIORITY“ sowie
die Methoden „getPriority ()“ und „setPriority ()“ zur Verfügung. Alle Threads, deren Priorität
nicht explizit mit „setPriority ()“ geändert worden ist, befinden sich in der Prioritätsstufe
NORM_PRIORITY. In der Regel werden unter Java die Prioritätsstufen eins bis zehn zur Verfügung gestellt, wobei die Stufe fünf die Standardpriorität ist. Die virtuelle Java-Maschine kann die
Prioritätsstufen auf beliebige Weise implementieren. Sie kann die zehn Prioritätsstufen beispielsweise auf zehn oder weniger Prioritätsstufen des Betriebssystems abbilden oder die Werte auch
vollständig ignorieren. Unter Solaris werden die Prioritätsstufen in „Java 5“ z. B. auf fünf Prioritätsstufen des Betriebssystems abgebildet. Die Prioritätsstufen fünf bis zehn werden alle auf die
höchste Prioritätsstufe in der TS- oder IA-Klasse (timesharing, interactive) abgebildet und die
restlichen vier Prioritätsstufen auf entsprechende niedrigere Prioritätsstufen dieser Klassen
(http://docs.oracle.com/javase/1.5.0/docs/guide/vm/thread-priorities.html). Sie dürfen sich in
Ihren Programmen also nicht auf die Prioritätsstufen verlassen! Die Programme im Verzeichnis
ThreadScheduling demonstrieren die Arbeit mit Prioritäten.
/* This program demonstrates the scheduling of threads in Java. The
* ...
* File: ThreadSchedulingMain.java
Author: S. Gross
*/
public class ThreadSchedulingMain
{
public static void main (String args[])
{
...
(new PrintThread (++thrID, TEXT_1)).start ();
(new PrintThread (++thrID, TEXT_2)).start ();
if ((cmd.compareTo ("prio") == 0))
{
HighPriorityThread prio = new HighPriorityThread (++thrID, SLEEP_TIME);
prio.setPriority (Thread.MAX_PRIORITY);
prio.start ();
}
...
}
}
/* This program demonstrates the scheduling of threads in Java.
* ...
* File: PrintThread.java
Author: S. Gross
*/
class PrintThread extends Thread
{
private int
thrID;
private String message;
public PrintThread (int thrID, String message)
{
...
}
public void run ()
{
String name = getName ();
Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
Praktikum zur Lehrveranstaltung Betriebssysteme
Seite 4 - 13
/* decrease the priority so that the main thread is able to exit
* even then when no round-robin scheduler is implemented.
*/
this.setPriority (Thread.NORM_PRIORITY - 1);
while (true)
{
for (int i = 0; i < 500000; ++i) /* simulate some work
*/
{
Math.random ();
}
System.out.println (name + ": priority: " + this.getPriority () +
"
" + message);
}
}
}
/* This program demonstrates the scheduling of threads in Java.
* ...
* File: HighPriotityThread.java
Author: S. Gross
*/
class HighPriorityThread extends Thread
{
private int thrID;
private long sleeptime;
public HighPriorityThread (int thrID, long sleeptime)
{ ...
}
public void run ()
{
String name = getName ();
while (true)
{
/* The thread prints a message so that you can see that it was
* invoked. Normally it won't print anything.
*/
System.out.println (name + ": priority: " + this.getPriority ());
try
{
sleep (sleeptime);
} catch (InterruptedException e)
{
}
}
}
}
Die Klasse Thread stellt u. a. die folgenden Methoden zur Verfügung.
static int activeCount ()
aktuelle Anzahl Threads in der Gruppe des Threads
static Thread currentThread ()
liefert Referenz auf das aktuelle Thread-Objekt
static int enumerate (Thread[] tarray)
liefert Referenzen auf alle aktiven Thread-Objekte in
der Gruppe und allen Untergruppen des aktuellen
Threads; liefert die Anzahl der Einträge im Feld als
Rückgabewert
long getID ()
liefert den Identifikator des Threads (entspricht dem
Identifikator beim Erzeugen des Threads)
Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
Praktikum zur Lehrveranstaltung Betriebssysteme
Seite 4 - 14
String getName ()
liefert den Namen des Threads
int getPriority ()
liefert die Priorität des Threads
ThreadGroup getThreadGroup ()
liefert die Thread-Gruppe, zu der der Thread gehört
void interrupt ()
unterbricht den Thread
static boolean interrupted ()
testet, ob der Thread unterbrochen wurde („interrupt
status flag“ wird gelöscht)
booelean isAlive ()
testet, ob der Thread noch „lebt“
boolean isDaemon ()
testet, ob Thread als Dämon läuft
boolean isInterrupted ()
testet, ob der Thread unterbrochen wurde („interrupt
status flag“ wird nicht verändert)
void join ()
wartet auf das Ende des Threads
void join (long ms)
wartet höchstens ms Millisekunden auf das Ende des
Threads
void setDaemon (boolean on)
schaltet zwischen daemon thread und user thread um
(muss aufgerufen werden, bevor der Thread gestartet
wird)
void setName (String name)
ändert den Namen des Threads
void setPriority (int np)
setzt Priorität des Threads auf np
static void sleep (long ms)
Thread „schläft“ ms Millisekunden
void start ()
Thread beginnt mit Ausführung
String toString ()
liefert Beschreibung des Threads, im Allgemeinen
(Name, Gruppe, Priorität, ...)
static void yield ()
Thread verzichtet temporär auf Prozessor, sodass
andere Threads ausgeführt werden können
Die Arbeit der Threads muss synchronisiert werden, wenn sie mit gemeinsamen Variablen arbeiten. Java bietet hierfür eine Synchronisation über Monitore, die sicherstellt, dass alle Anweisungsblöcke und Methoden, die durch das Schlüsselwort „synchronized“ geschützt werden, zu
jedem Zeitpunkt von höchstens einem Thread benutzt werden können (der Zugriff wird sequentialisiert). Falls ein Thread innerhalb einer solchen Methode auf eine Bedingung warten muss (z.
B. auf einen freien Speicherplatz oder einen Datensatz beim Erzeuger-/Verbraucherproblem),
kann er in der geschützten Methode die Methode „wait ()“ aus der Klasse Object von java.lang
aufrufen. Wenn er durch den Aufruf von „wait ()“, „wait (long timeout)“ oder „wait (long timeout, int nanos)“ blockiert worden ist, werden automatisch alle Sperren, die er in diesem MonitorObjekt hatte, freigegeben und er nimmt am Scheduling nicht mehr teil. Er wird aus der Warteschlange befreit, wenn ein anderer Thread für das Monitor-Objekt „notify ()“ oder „notifyAll ()“
aufruft oder ein anderer Thread für ihn die Methode „interrupt ()“ aufruft oder die über den Parameter der wait-Methode eingestellte Zeit abgelaufen ist. In Java warten alle Threads in derselben Objekt-Warteschlange, d. h., dass ein aufgeweckter Thread erneut mit allen anderen Threads
Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
Praktikum zur Lehrveranstaltung Betriebssysteme
Seite 4 - 15
um den Zutritt zum Monitor-Objekt kämpfen muss, nachdem das Monitor-Objekt freigegeben
worden ist (ein Thread, der in einem Monitor-Objekt blockiert war, hat keine Priorität gegenüber
einem Thread, der den Monitor zum ersten Mal benutzen will). Wenn ein blockierter Thread
aufgeweckt worden ist, kann er deshalb nicht davon ausgehen, dass die Bedingung noch erfüllt
ist, wenn er den Monitor wieder besitzt. Sobald er den Monitor wieder betreten hat, werden alle
Sperren wieder in denselben Zustand versetzt, den sie zum Zeitpunkt des Aufrufs der Methode
„wait ()“ hatten. Falls der Thread durch „interrupt ()“ geweckt worden ist, wird die Ausnahme
InterruptedException nach Wiederherstellung seines Zustands im Monitor ausgelöst. Die Methode „wait ()“ gibt nur die Sperren für das Objekt frei, in dem sie aufgerufen wurde, d. h., dass alle
anderen Objekte, die der Thread eventuell noch gesperrt hatte, gesperrt bleiben, während er in
der Warteschlange wartet. Durch Mehrfachsperren können also sehr leicht Verklemmungen
(deadlocks) entstehen. Die Methode „notify ()“ befreit einen beliebigen blockierten Thread aus
der Warteschlange (implementierungsabhängig), während „notifyAll ()“ alle blockierten Threads
aus der Objekt-Warteschlange befreit.
Falls Threads auf verschiedene Bedingungen warten, muss „notifyAll ()“ benutzt werden, da
sonst u. U. ein „falscher“ Thread geweckt wird, wie das folgende Beispiel zeigt. Betrachten wir
ein Erzeuger-/Verbraucherproblem mit zwei Erzeugern, einem Verbraucher, einem Puffer für
einen Datensatz sowie einer FIFO-Auswahlstrategie. Der Verbraucher kann zwei aufeinanderfolgende Pufferzugriffe machen, da ihm der Prozessor nicht entzogen wurde und er sich auch
nicht selbst blockiert hat, sodass P1 den Monitor noch nicht wieder betreten konnte.
Aufgabe
Puffer
wait-WS
Monitor-WS
Aktionen
-
leer
-
-
-
P1 legt Datensatz ab
voll
-
-
notify ohne Wirkung
P1 will Datensatz ablegen
voll
P1
-
P1 wird blockiert
P0 will Datensatz ablegen
voll
P1, P0
-
P0 wird blockiert
C0 liest Datensatz
leer
P0
P1
notify befreit P1
C0 will Datensatz lesen
leer
P0, C0
P1
C0 wird blockiert
P1 legt Datensatz ab
voll
C0
P0
notify befreit P0
P0 will Datensatz ablegen
voll
C0, P0
-
P0 wird blockiert
P1 will Datensatz ablegen
voll
C0, P0, P1
-
P1 wird blockiert
⇒ Blockade aller Threads
Die Methode notifyAll hätte die Blockade vermieden, da sie immer alle blockierten Threads
aufgeweckt hätte. Der Verbraucher hätte den Datensatz dann aus dem Puffer entnommen und
damit den Fortschritt der Erzeuger garantiert. Im Verzeichnis Threads/ProducerConsumer
befinden sich die entsprechend modifizierten Programme aus dem Programmarchiv der Hauptveranstaltung Betriebssysteme (zwei Erzeuger, ein Verbraucher, Puffer für einen Datensatz,
notify statt notifyAll, aktives Warten). Übersetzen und starten Sie das Programm und warten Sie
dann bis ggf. eine Blockade eintritt. Es hängt von der Java-Implementierung ab, ob es während
der Laufzeit des Programms tatsächlich zu einer Blockade kommt.
Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
Praktikum zur Lehrveranstaltung Betriebssysteme
Seite 4 - 16
Die Threads im Programm des Verzeichnisses SyncBlock erhöhen den Wert einer gemeinsamen
Variablen und geben den Wert der Variablen aus. Wenn der Zugriff auf die gemeinsame Variable
synchronisiert wird (SyncBlockOneMain.java), werden, wie erwartet, nur verschiedene Werte
ausgegeben. Falls der Zugriff nicht synchronisiert wird (SyncBlockTwoMain.java), werden einige
Werte mehrfach ausgegeben, da derselbe Wert von mehreren Threads gelesen, inkrementiert und
zurückgeschrieben wurde. Nachdem Sie das Programm übersetzt haben, können Sie es unter
UNIX mit dem folgenden Befehl ausführen, der nur mehrfach vorkommende Zahlen ausgibt
(„man awk“ und „man uniq“ liefern Ihnen Details zu den beiden UNIX-Befehlen).
java SyncBlockOneMain | awk '{ print $2 }' | sort | uniq -D bzw.
java SyncBlockTwoMain | awk '{ print $2 }' | sort | uniq -D
/* Monitors in Java: synchronizing a block within a method.
* ...
* File: SyncBlockOneMain.java
Author: S. Gross
*/
public class SyncBlockOneMain
{
public static void main (String args[])
{
final long WAIT_TIME = 5000;
/* 5 seconds
int
i
= 0;
/* thread ID
SyncBlockOneThread thr1 = new SyncBlockOneThread (++i);
SyncBlockOneThread thr2 = new SyncBlockOneThread (++i);
SyncBlockOneThread thr3 = new SyncBlockOneThread (++i);
thr1.start ();
thr2.start ();
thr3.start ();
/* wait some time before terminating all threads
try
{
Thread.sleep (WAIT_TIME);
} catch (InterruptedException e)
{
}
System.exit (0);
*/
*/
*/
}
}
/* Monitors in Java: synchronizing a block within a method.
* ...
* File: SyncBlockOneThread.java
Author: S. Gross
*/
class SyncBlockOneThread extends Thread
{
private int thrID;
/* ID of this thread
static int cnt = 0;
/* shared variable
public SyncBlockOneThread (int thrID)
{
this.thrID = thrID;
setName ("SyncBlockOneThread-" + Integer.toString (thrID));
}
public void run ()
{
String name = getName ();
Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
*/
*/
Praktikum zur Lehrveranstaltung Betriebssysteme
Seite 4 - 17
while (true)
{
synchronized (getClass ())
{
System.out.println (name + ": " + cnt);
cnt = cnt + 1;
/* long way to catch an error
}
}
*/
}
}
Der Synchronisationsfehler tritt auch dann auf, wenn Sie „cnt = cnt + 1“ durch „cnt++“ ersetzen.
Im Verzeichnis SyncBlockRunnable wurden die entsprechenden Programme über „implements
Runnable“ implementiert. Die Programme im Verzeichnis SyncMethod demonstrieren die
Synchronisation beim Zugriff auf Methoden. Die Methode „nextInt ()“ soll fortlaufende Zahlen
liefern. Wenn der Zugriff nicht synchronisiert wird, erhalten einige Threads dieselbe Zahl.
Starten Sie die Programme über die folgenden Befehle, damit Sie die Fehlfunktion ggf. einfach
sehen können.
java SyncMethodOneMain | awk '{ print $2 }' | sort | uniq -D bzw.
java SyncMethodTwoMain | awk '{ print $2 }' | sort | uniq -D
/* Monitors in Java: synchronizing a method.
* ...
* File: SyncMethodOneMain.java
Author: S. Gross
*/
public class SyncMethodOneMain
{
public static void main (String args[])
{
final int
THR_NUM
= 4;
/* number of threads
final int
FIRST_VAL = 0;
/* start value
final long
WAIT_TIME = 5000;
/* 5 seconds
SyncMethodOneThread thr[]
= new SyncMethodOneThread[THR_NUM];
SequentialIntOne
val
= new SequentialIntOne (FIRST_VAL);
for (int i = 0; i < THR_NUM; ++i)
{
thr[i] = new SyncMethodOneThread (i, val);
thr[i].start ();
}
/* wait some time before terminating all threads
try
{
Thread.sleep (WAIT_TIME);
} catch (InterruptedException e)
{
}
System.exit (0);
*/
*/
*/
*/
}
}
/* Monitors in Java: synchronizing a method.
* ...
* File: SyncMethodOneThread.java
Author: S. Gross
*/
class SyncMethodOneThread extends Thread
{
private int
thrID;
/* ID of this thread
private SequentialIntOne val;
Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
*/
Praktikum zur Lehrveranstaltung Betriebssysteme
Seite 4 - 18
public SyncMethodOneThread (int thrID, SequentialIntOne val)
{
this.thrID = thrID;
this.val
= val;
setName ("SyncMethodOneThread-" + Integer.toString (thrID));
}
public void run ()
{
String name = getName ();
while (true)
{
System.out.println (name + ": " + val.nextInt ());
}
}
}
/* Monitors in Java: synchronizing a method.
* ...
* File: SequentialIntOne.java
Author: S. Gross
*/
class SequentialIntOne
{
int val;
public SequentialIntOne (int val)
{
this.val = val;
}
public synchronized int nextInt ()
{
int oldInt;
oldInt = val;
for (int i = 0; i < 5000; ++i) /* simulate computation of new value */
{
Math.random ();
}
val++;
return oldInt;
}
}
Jeder Thread durchläuft in seinem Leben mehrere Zustände. Das folgende Diagramm zeigt die
Zustände eines Threads in der virtuellen Java-Maschine, wie sie von der Methode „getState ()“
geliefert werden.
new
runnable
waiting
timed_waiting
blocked
terminated
Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
Praktikum zur Lehrveranstaltung Betriebssysteme
Seite 4 - 19
Der Thread befindet sich im Zustand „new“, wenn das Thread-Objekt erzeugt worden ist, aber
noch nicht gestartet wurde. Sobald der Thread gestartet worden ist, befindet er sich im Zustand
„runnable“, d. h., dass er in der virtuellen Maschine ausgeführt werden kann. Unter Umständen
muss er aber noch auf Betriebsmittel des Betriebssystems warten (z. B. die CPU). Wenn er z. B.
darauf wartet, einen Monitor zu betreten, befindet er sich im Zustand „blocked“. Er befindet sich
im Zustand „waiting“, wenn er beispielsweise „wait ()“ oder „join ()“ aufgerufen hat und im
Zustand „timed_waiting“, wenn er z. B. „wait ()“ oder „join ()“ mit einem Zeitparameter oder
„sleep ()“ aufgerufen hat. Wenn er seine Arbeit beendet hat, befindet er sich im Zustand
„terminated“. Die Thread-Zustände können seit Java 5 mit der Methode „getState ()“ abgefragt
werden. Übersetzen und starten Sie das Programm „ThreadStateMain.java“ im Verzeichnis
ThreadState.
/* Program shows the states of a thread in the "Java Virtual Machine".
* ...
* File: ThreadStateMain.java
Author: S. Gross
*/
public class ThreadStateMain
{
public static void main (String args[])
{
final int LOOP_COUNT = 20;
final int SLEEP_TIME = 1000;
/* 1 second
int thrID = 0;
/* thread ID
*/
*/
CountThread thr1 = new CountThread (++thrID);
CountThread thr2 = new CountThread (++thrID);
System.out.println (thr1.getName () + ": " + thr1.getState ());
thr1.start ();
thr2.start ();
System.out.println (thr1.getName () + ": " + thr1.getState ());
for (int i = 0; i < LOOP_COUNT; ++i)
{
System.out.println (thr1.getName () + ": " + thr1.getState ());
try
{
Thread.sleep (SLEEP_TIME);
} catch (InterruptedException e)
{
}
}
}
}
/* Program shows the states of a thread in the "Java Virtual Machine".
* ...
* File: CountThread.java
Author: S. Gross
*/
class CountThread extends Thread
{
...
public void run ()
{
final int MAX_COUNT = 8;
final int SLEEP_TIME = 1000;
/* 1 second
Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
*/
Praktikum zur Lehrveranstaltung Betriebssysteme
Seite 4 - 20
String name = getName ();
for (int i = 0; i < MAX_COUNT; ++i)
{
try
{
Thread.sleep (SLEEP_TIME);
} catch (InterruptedException e)
{
}
synchronized (getClass ())
{
System.out.println (name + ": " + cnt);
cnt++;
try
{
Thread.sleep (SLEEP_TIME);
} catch (InterruptedException e)
{
}
}
}
}
public String toString ()
{
return getName ();
}
}
Neben den bisher besprochenen Threads (den sogenannten „user threads“) gibt es in Java noch
„daemon threads“. Diese Threads werden im Allgemeinen für Server eingesetzt, da sie automatisch enden, wenn der letzte „user thread“ geendet hat. In Java endet ein Programm erst dann,
wenn der letzte Thread beendet worden ist. Falls ein Thread also in einer Endlosschleife läuft (z.
B. ein Server-Thread), muss er beispielsweise mit „interrupt ()“ beendet werden oder das Hauptprogramm kann „System.exit (0)“ aufrufen und damit die virtuelle Maschine mit allen Threads
beenden. Eine weitere Möglichkeit besteht darin, solche Threads als „daemon threads“ zu starten. Die Methode „setDaemon ()“ muss aufgerufen werden, bevor der Thread gestartet wird. Den
Unterschied zwischen „user threads“ und „daemon threads“ zeigen die Programme im Verzeichnis ThreadDaemon. Übersetzen und starten Sie die beiden Hauptprogramme.
/* Program shows the behaviour of daemon threads compared to
* user threads.
* ...
* File: ThreadDaemonTrueMain.java
Author: S. Gross
*/
public class ThreadDaemonTrueMain
{
public static void main (String args[])
{
final int WAIT_TIME = 2000;
/* 2 seconds
DaemonUserThread thr = new DaemonUserThread ();
thr.setDaemon (true);
thr.start ();
System.out.println ("I've started a daemon thread");
Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
*/
Praktikum zur Lehrveranstaltung Betriebssysteme
Seite 4 - 21
try
{
Thread.sleep (WAIT_TIME);
} catch (InterruptedException e) { }
System.out.println ("Nothing more todo.");
}
}
/* Program shows the behaviour of daemon threads compared to
* user threads.
* ...
* File: DaemonUserThread.java
Author: S. Gross
*/
class DaemonUserThread extends Thread
{
public void run ()
{
final int SLEEP_TIME = 1000;
/* 1 second
*/
while (true)
{
if (isDaemon ())
{
System.out.println (getName () + ": Running as daemon thread");
}
else
{
System.out.println (getName () + ": Running as user thread");
}
try
{
Thread.sleep (SLEEP_TIME);
} catch (InterruptedException e) { }
}
}
}
Wenn Sie das Programm ThreadDaemonFalseMain starten, läuft der Thread beliebig lange
weiter und Sie müssen das Programm z. B. mit <Strg-c> zwangsweise beenden.
eiger ThreadDaemon 149 java ThreadDaemonFalseMain
I've started a user thread
DaemonUserThread: Running as user thread
DaemonUserThread: Running as user thread
Nothing more todo.
DaemonUserThread: Running as user thread
...
Wenn Sie das Programm ThreadDaemonTrueMain starten, läuft der Thread nur solange wie das
Hauptprogramm.
eiger ThreadDaemon 150 java ThreadDaemonTrueMain
I've started a daemon thread
DaemonUserThread: Running as daemon thread
DaemonUserThread: Running as daemon thread
Nothing more todo.
eiger ThreadDaemon 151
Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
Praktikum zur Lehrveranstaltung Betriebssysteme
Seite 4 - 22
Zum Schluss wollen wir uns noch einmal mit dem kleinen Grafikprogramm vom Anfang des
Kapitels beschäftigen. Dieses Programm hat zwar anschaulich gezeigt, dass Threads sinnvoll
sind, ist aber gleichzeitig eine vollkommen falsche und sogar gefährliche Lösung für unsere
Animation, da die Swing-Routinen nicht „thread-safe“ sind. Das Hauptprogramm der Animation
sorgt dafür, dass ein Fenster mit den entsprechenden Knöpfen (Buttons) erzeugt wird. Nachdem
das Fenster mit allen Komponenten erzeugt worden ist, wird es sichtbar gemacht (z. B. mit der
Methode „setVisible (true)“. In diesem Moment wird von Java automatisch ein Thread zur Ereignisverwaltung (der sogenannte „Event Dispatch Thread“) gestartet. Nur dieser Thread darf von
jetzt an Änderungen am Fensterinhalt vornehmen. Der „Event Dispatch Thread“ wird als normaler „user thread“ gestartet, sodass das Hauptprogramm z. B. explizit „System.exit (0)“ aufrufen
sollte, wenn es beendet werden soll. Das Programm vom Anfang dieses Kapitels meldet in dem
Fenster, in dem es gestartet wurde, was gerade passiert. Wie Sie der Ausgabe entnehmen können,
bewegt der Thread zur Ereignisverwaltung mit dem Namen „AWT-EventQueue-0“ die Bälle. Da
der Thread mit dem Ball beschäftigt ist, kann er natürlich nicht sofort auf Eingaben im Fenster
reagieren.
eiger MoveBall 6 javac MoveBallMain.java
eiger MoveBall 7 java MoveBallMain
AWT-EventQueue-0: new ball; color: blue; position: (47,322); step size: (2,6)
AWT-EventQueue-0: new ball; color: blue; position: (495,26); step size: (6,6)
eiger MoveBall 8
In den Thread-Programmen wird der Ball von MoveBall-Threads bewegt und der Thread zur
Ereignisverwaltung kann sich seiner eigentlichen Aufgabe widmen und auf Ereignisse im Fenster
reagieren. Da die MoveBall-Threads ihre Änderungen direkt in der Zeichenfläche des Fensters
vornehmen, entsprechen sie nicht der Philosophie und Architektur von Swing-Programmen. In
dem kleinen Beispiel ist es zwar nie zu schwerwiegenden Fehlern gekommen, aber laut Dokumentation können derartige Fehler auftreten. Wenn Sie den Ball vergrößern, erkennen Sie weitere Schwächen des Programms (im Verzeichnis MoveBigBallThreadGroup).
Da die Bälle direkt in der Zeichenfläche erstellt werden, flimmert das Bild. Eine flimmerfreie
Lösung benötigt eine Zwischenpufferung der Zeichnung (sogenanntes „double buffering“). Die
Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
Praktikum zur Lehrveranstaltung Betriebssysteme
Seite 4 - 23
Bälle werden mit der XOR-Funktion gezeichnet, damit das Programm einfach implementiert
werden konnte. Wie Sie der obigen Grafik entnehmen können, hat das zur Folge, dass neue Ballfarben entstehen oder Teile eines Balls in der Hintergrundfarbe gezeichnet werden, wenn sich
Bälle überlappen. Wenn Sie viele Bälle starten, wird die Anwendung manchmal sehr langsam
und der „Event Dispatch Thread“ reagiert wieder sehr schlecht auf Änderungswünsche (z. B.
Fenstergröße ändern oder „Close“ klicken). Wenn Sie in der Methode „run ()“ der Datei
„MoveBall.java“ die Konstante „SLEEP_TIME“ auf z. B. 5000 (fünf Sekunden) erhöhen, einen
Ball starten und dann die Fenstergröße ändern, passiert erst einmal nichts. Wenn die Zeit abgelaufen ist, wird der Ball erneut gezeichnet. Normalerweise sollte der Fensterinhalt sofort aktualisiert werden. Wenn sich die Größe des Fensters ändert oder ein Fenster bzw. ein Teil eines Fensters aus dem Hintergrund in den Vordergrund bewegt wird, ruft der „Event Dispatch Thread“
automatisch die Methode „paint ()“ auf. Es bietet sich deshalb an, diese Methode zu überschreiben und alle Änderungen des Fensterinhalts nur in dieser Methode vorzunehmen. Wenn ein Teil
der Grafik neu erstellt worden ist, kann z. B. die Methode „repaint ()“ aufgerufen werden und
damit ein Neuzeichnen des Fensterinhalts erzwungen werden. Da das Fenster verschiedene
Objekte enthalten kann, ist es auch sinnvoll, dass sich die Objekte selbst in einen Zwischenpuffer
zeichnen, der dann später in das Fenster kopiert wird. Die Objekte könnten beispielsweise in
einer Liste verwaltet werden. Falls die Grafik sehr kompliziert und damit rechenintensiv ist, ist es
im Allgemeinen auch sinnvoll, dass nicht der gesamte Fensterinhalt neu gezeichnet wird, sondern
nur der Teil, der geändert oder aus dem Hintergrund in den Vordergrund geholt wurde. Bei dieser
einfachen Grafik können wir auf zusätzliche Threads vollständig verzichten, da die Animation
auch über eine Uhr (Timer) realisiert werden kann. Im Verzeichnis MoveBallFinalVersion finden Sie eine bessere Lösung für die Animation. Der Ball wird jetzt „nur noch“ als Objekt
implementiert.
/* Let a ball bounce around within a canvas. The coordinate (0, 0)
* is in the upper left corner of the window.
* ...
* File: BallTwo.java
Author: S. Gross
*/
import javax.swing.*;
import java.awt.*;
public class BallTwo
{
private static final
private static final
private static final
private static final
private
private
private
private
private
private
private
JPanel
Color
Graphics
Dimension
int
int
int
/* needed for Swing classes
/* needed for Colors
int
int
int
int
XSIZE
YSIZE
STEP_SIZE_X
STEP_SIZE_Y
canvas;
color;
g;
d;
x, y;
dx, dy;
ballNum;
=
=
=
=
80;
80;
6;
6;
/*
/*
/*
/*
/*
/*
/*
Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
*/
*/
/* size of ball
*/
/* max. step size for
/*
new ball position
*/
*/
drawing area
color of ball
current graphics context
size of canvas
current position of ball
increments to move the ball
ID of ball
*/
*/
*/
*/
*/
*/
*/
Praktikum zur Lehrveranstaltung Betriebssysteme
Seite 4 - 24
public BallTwo (JPanel canvas, Color color, int id)
{
this.canvas = canvas;
this.color
= color;
this.ballNum = id;
this.g
= canvas.getGraphics ();
this.d
= canvas.getSize ();
/* compute random speed of ball. "+ 1" avoids balls running on
* horizontal or vertical lines
*/
this.dx
= (int) (Math.random () * STEP_SIZE_X) + 1;
this.dy
= (int) (Math.random () * STEP_SIZE_Y) + 1;
/* draw first ball at random starting point
this.x
= (int) (Math.random () * d.width);
this.y
= (int) (Math.random () * d.height);
}
*/
public BallTwo (JPanel canvas, Color color, int id, int x, int y)
{
this.canvas = canvas;
this.color
= color;
this.ballNum = id;
this.x
= x;
this.y
= y;
this.g
= canvas.getGraphics ();
this.d
= canvas.getSize ();
/* compute random speed of ball. "+ 1" avoids balls running on
* horizontal or vertical lines
*/
this.dx = (int) (Math.random () * STEP_SIZE_X) + 1;
this.dy = (int) (Math.random () * STEP_SIZE_Y) + 1;
}
public void paintBall (Graphics g)
{
/* check if ball is inside the canvas (possibly the size
* of the canvas has changed) -> adjust position if necessary
* so that ball is still inside the canvas
*/
checkUpdateBallPosition ();
g.setColor (color);
g.fillOval (x, y, XSIZE, YSIZE);
}
/* compute new ball position
public void newBallPosition ()
{
x += dx;
y += dy;
}
...
*/
/* new ball position
*/
}
Falls die Methode „paintBall ()“ umfangreiche Berechnungen durchführen müsste, könnte man
diese Arbeiten auch in einem separaten Thread durchführen lassen. Die Methode „paintBall ()“
wird in der Methode „paint ()“ der Klasse „MyJPanelTwo“ aufgerufen, wie Sie dem folgenden
Programm entnehmen können. In dieser Klasse wird auch ein Uhren-Thread gestartet, der dafür
sorgt, dass der Fensterinhalt regelmäßig aktualisiert wird.
Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
Praktikum zur Lehrveranstaltung Betriebssysteme
Seite 4 - 25
/* Draw some objects.
* ...
* File: MyJPanelTwo.java
Author: S. Gross
*/
...
public class MyJPanelTwo extends JPanel
{
static final long serialVersionUID = 1126359778513707255L;
private ArrayList<BallTwo> ballsList
= new ArrayList<BallTwo> ();
public MyJPanelTwo ()
{
final int DELAY = 5;
/* use a timer thread to update the screen
PaintBallsListener paintBalls = new PaintBallsListener ();
Timer
t
= new Timer (DELAY, paintBalls);
t.start ();
}
public void paint (Graphics g)
{
super.paint (g);
for (BallTwo b: ballsList)
{
b.paintBall (g);
}
}
public void addBall (BallTwo b)
{
ballsList.add (b);
System.out.println ("New " + b.toString ());
repaint ();
}
public void removeBalls (String color)
{
BallTwo b;
Iterator<BallTwo> it = ballsList.iterator ();
while (it.hasNext ())
{
b = it.next ();
if (b.getBallColor() == color)
{
it.remove();
}
}
repaint ();
}
private class PaintBallsListener implements ActionListener
{
public void actionPerformed (ActionEvent event)
{
for (BallTwo b: ballsList)
{
b.newBallPosition ();
}
repaint ();
}
}
}
Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
*/
Praktikum zur Lehrveranstaltung Betriebssysteme
Seite 4 - 26
Der folgenden Datei können Sie entnehmen, wie das Fenster mit der Zeichenfläche und den verschiedenen Routinen zur Behandlung von Ereignissen erzeugt wird.
/* Create a window with a drawing area (canvas) at the top of the
* windows and the following buttons at the bottom.
*
Start blue ball
start a blue ball moving within the window
*
Start red ball
start a red ball moving within the window
*
Remove blue balls remove all blue balls
*
Remove red balls
remove all red balls
*
Close
close the window
* This version installs a MouseListener for the drawing area. A
* click with any button creates a "green ball". A "double click"
* removes all green balls.
* ...
* File: WindowWithButtonsTwo.java
Author: S. Gross
*/
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
/* needed for Swing classes
/* needed for Container
/* needed for ActionListener
*/
*/
*/
public class WindowWithButtonsTwo extends JFrame
implements MouseListener
{
static final long serialVersionUID = -4612637987030198879L;
private MyJPanelTwo canvas;
private int
ballNum;
/* drawing area
/* ID of ball
public WindowWithButtonsTwo ()
{
final String DEFAULT_TITLE
final int
DEFAULT_WINDOW_WIDTH
final int
DEFAULT_WINDOW_HEIGHT
final int
UPPER_LEFT_WINDOW_EDGE_X
final int
UPPER_LEFT_WINDOW_EDGE_Y
=
=
=
=
=
*/
*/
"Bouncing Ball";
640;
480;
100;
40;
ballNum = 1;
setTitle (DEFAULT_TITLE);
setSize (DEFAULT_WINDOW_WIDTH, DEFAULT_WINDOW_HEIGHT);
setLocation (UPPER_LEFT_WINDOW_EDGE_X, UPPER_LEFT_WINDOW_EDGE_Y);
/* specify what happens when the window will be closed
setDefaultCloseOperation (JFrame.EXIT_ON_CLOSE);
/* build container with canvas and panel with buttons
buildContainer ();
setVisible (true);
/* display the window
*/
*/
*/
}
/* The window will be created with the BorderLayout. Since "North",
* "East", and "West" are unused, "Center" will use the whole top
* of the window. The buttons are in "South".
*/
private void buildContainer ()
{
Container canvasAndButtonPanel;
JPanel
buttonPanel;
JButton
startBlueBallButton, startRedBallButton,
removeBlueBallButton, removeRedBallButton, closeButton;
canvasAndButtonPanel = getContentPane ();
/* create and add canvas
Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
*/
Praktikum zur Lehrveranstaltung Betriebssysteme
Seite 4 - 27
canvas = new MyJPanelTwo ();
canvasAndButtonPanel.add (canvas, "Center");
/* create buttons and add action listener
startBlueBallButton = new JButton ("Start blue ball");
startRedBallButton
= new JButton ("Start red ball");
removeBlueBallButton = new JButton ("Remove blue balls");
removeRedBallButton = new JButton ("Remove red balls");
closeButton
= new JButton ("Close");
startBlueBallButton.addActionListener (new
StartBlueBallButtonListener ());
startRedBallButton.addActionListener (new
StartRedBallButtonListener ());
removeBlueBallButton.addActionListener (new
RemoveBlueBallButtonListener ());
removeRedBallButton.addActionListener (new
RemoveRedBallButtonListener ());
closeButton.addActionListener (new CloseButtonListener ());
/* create panel and add buttons
buttonPanel = new JPanel ();
buttonPanel.add (startBlueBallButton);
buttonPanel.add (startRedBallButton);
buttonPanel.add (removeBlueBallButton);
buttonPanel.add (removeRedBallButton);
buttonPanel.add (closeButton);
/* add panel to content pane
canvasAndButtonPanel.add (buttonPanel, "South");
/* add MouseListener only to drawing area
canvas.addMouseListener (this);
*/
*/
*/
*/
}
private class StartBlueBallButtonListener implements ActionListener
{
public void actionPerformed (ActionEvent event)
{
BallTwo blueBall = new BallTwo (canvas, Color.blue, ballNum++);
canvas.addBall (blueBall);
}
}
...
private class RemoveBlueBallButtonListener implements ActionListener
{
public void actionPerformed (ActionEvent event)
{
canvas.removeBalls ("blue");
}
}
...
private class CloseButtonListener implements ActionListener
{
public void actionPerformed (ActionEvent event)
{
System.exit (0);
/* stop everything
}
}
/* implement MouseListener methods
public void mousePressed (MouseEvent event)
{
}
public void mouseReleased (MouseEvent event)
{
}
Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
*/
*/
Praktikum zur Lehrveranstaltung Betriebssysteme
Seite 4 - 28
public void mouseClicked (MouseEvent event)
{
switch (event.getClickCount ())
{
case 1:
/* single mouse click
BallTwo greenBall = new BallTwo (canvas, Color.green,
ballNum++, event.getX (),
event.getY ());
canvas.addBall (greenBall);
break;
case 2:
canvas.removeBalls ("green");
break;
default:
;
*/
/* double click
*/
/* do nothing
*/
}
}
public void mouseEntered (MouseEvent event)
{
}
public void mouseExited (MouseEvent event)
{
}
}
Jetzt muss das Fenster noch über ein Hauptprogramm erzeugt werden und das Programm ist
fertig.
/* Program creates a window which allows to start a ball moving
* within that window and to close the windows.
* ...
* File: MoveBallTwoMain.java
Author: S. Gross
*/
public class MoveBallTwoMain
{
public static void main (String args[])
{
WindowWithButtonsTwo myWindow = new WindowWithButtonsTwo ();
}
}
Die Programme „*One*.java“ verwalten eine Objektliste für jede Farbe während die Programme
„*Two*.java“ nur eine Liste für alle Objekte benutzen. Verschiedene Objektlisten haben den
Vorteil, dass man alle Objekte eines Typs sehr einfach löschen kann (kein „Iterator“ erforderlich). Da die Listen aber nacheinander gezeichnet werden, sind die Objekte der letzten Liste
immer „oben“. Falls nur eine Objektliste benutzt wird, werden die Objekte in der Reihenfolge
gezeichnet, in der sie erzeugt worden sind. Das folgende Bild ist eine Aufnahme des Programms
„MoveBallOneMain.java“.
Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
Praktikum zur Lehrveranstaltung Betriebssysteme
Seite 4 - 29
Das folgende Bild ist eine Aufnahme des Programms „MoveBallTwoMain.java“.
Obwohl Swing automatisch „double buffering“ benutzt, flackert ein großer Ball am Rand, wenn
er große Schrittweiten benutzt. Mir ist nicht klar, ob es sich um einen konzeptionellen Fehler in
meinem Programm oder ein Problem mit Java handelt. Eine Änderung der Aktualisierungsrate
(nur 10 statt 200 Bilder pro Sekunde) oder ein größerer Speicher für die virtuelle Maschine (java
-Xms512m -Xmx1024m) haben das Problem nicht gelöst. Falls die Schrittweite zwischen zwei
Ballpositionen kleiner als drei Pixel ist, ist kein Versatz im Ball erkennbar. Im Verzeichnis
„MoveBallAWT“ wurden die Programme mit Hilfe des AWT (abstract windows toolkit) implementiert (anstelle von JPanel wird die Klasse Panel benutzt). Im Programm „*One*.java“ flimmern die Bälle wieder, da AWT im Gegensatz zu Swing keine Doppelpufferung benutzt. Aus
diesem Grund wurde im Programm „*Two*.java“ eine Doppelpufferung implementiert.
Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
Praktikum zur Lehrveranstaltung Betriebssysteme
/* Draw some objects.
* ...
* File: MyPanelTwo.java
*/
...
Seite 4 - 30
Author: S. Gross
public class MyPanelTwo extends Panel
{
static final long serialVersionUID = 213104249712947171L;
private ArrayList<BallTwo> ballsList
= new ArrayList<BallTwo> ();
private Image
bufferedImage = null;
private Graphics
bg
= null;
...
/* "paint ()" will be called automatically every time the screen
* needs repainting , e.g., when you resize the window or the timer
* thread calls "repaint ()". In contrast to Swing the "Abstract
* Windows Toolkit" (AWT) doesn't support "double buffering" so
* that you must implement it yourself.
*
* "g" contains the current graphics context (fonts, clip area, ...).
*/
public void paint (Graphics g)
{
super.paint (g);
/* clean up the previous image
if (bufferedImage != null)
{
bufferedImage.flush ();
bufferedImage = null;
}
if (bg != null)
{
bg.dispose ();
bg = null;
}
/* create new image with the current window size
bufferedImage = createImage (getSize ().width,
getSize ().height);
bg
= bufferedImage.getGraphics ();
/* paint balls into offscreen buffer
for (BallTwo b: ballsList)
{
b.paintBall (bg);
}
/* paint buffered image to screen image
g.drawImage (bufferedImage, 0, 0, this);
}
...
*/
*/
*/
*/
}
Wenn Sie das Programm übersetzen und starten, flimmern die Bälle noch mehr als vorher. Der
Grund hierfür ist, dass das AWT nicht „paint ()“ sondern „update ()“ aufruft, wenn die Grafik
neu gezeichnet werden muss. „update ()“ löscht u. a. das Fenster, in dem die Grafik gezeichnet
werden soll, bevor sie „paint ()“ aufruft. Das Löschen des Hintergrunds vor dem Neuzeichnen
resultiert im starken Flimmern dieser Version des Programms. Im Programm „*Three*.java“
wird deshalb die Methode „update ()“ überschrieben, um das Flimmern zu verhindern.
Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
Praktikum zur Lehrveranstaltung Betriebssysteme
/* Draw some objects.
* ...
* File: MyPanelThree.java
*/
...
Seite 4 - 31
Author: S. Gross
public class MyPanelThree extends Panel
{
static final long serialVersionUID = 213104249712947171L;
private ArrayList<BallThree> ballsList
= new ArrayList<BallThree> ();
private Image
bufferedImage = null;
private Graphics
bg
= null;
...
/* "update ()" will be called automatically every time the screen
* needs repainting , e.g., when you resize the window or the timer
* thread calls "repaint ()". Swing doesn't use "update ()" but calls
* "paint ()" directly. Among other things "update ()" clears the
* panel and paints the new image by calling "paint ()". This caused
* the heavy flickering. When you overwrite "update ()" so that it
* just calls "paint ()" the flicker should be gone if "paint ()"
* uses "double buffering" (mainly because it doesn't clear the
* background any longer).
*
* "g" contains the current graphics context (fonts, clip area, ...).
*/
public void update (Graphics g)
{
paint (g);
}
/* "paint ()" will be called via "update ()". AWT doesn't support
* "double buffering" so that you must implement it yourself.
*
* "g" contains the current graphics context (fonts, clip area, ...).
*/
public void paint (Graphics g)
{
...
}
...
}
Im Programm „*Four*.java“ werden die Bälle nur um jeweils ein Pixel weiterbewegt und im
Programm „*Five*.java“ gibt es wieder kleine Bälle. Die korrekte AWT-Lösung hat dasselbe
Problem wie die korrekte Swing-Lösung, d. h., dass große Bälle bei großen Schrittweiten am
Rand flimmern.
Der kleine Exkurs in die Programmierung von Oberflächen hat natürlich nur am Rande etwas mit
Threads zu tun, da sie hier für bestimmte Aufgaben eingesetzt werden. Vielleicht sind die Informationen aber hilfreich und erleichtern Ihnen Ihr weiteres Studium.
Hochschule Fulda, Fachbereich AI, Prof. Dr. S. Groß
Herunterladen