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ß