11.8 | Zeitgesteuerte Ausführung von Threads |
InVersion 1.3 wurde die Standardbibliothek um zwei Klassen erweitert, die die Ausführung periodisch wiederkehrender Vorgänge (Tasks) ermöglichen. Es handelt sich hierbei um die Klassen Timer und TimerTask aus dem Paket java.util.
Timer hat dabei die Steuerfunktion und führt die einzelnen Tasks als Threads aus. TimerTask stellt einen einzelnen Task dar und implementiert das Interface Runnable, damit die einzelnen Tasks als Threads ausgeführt werden können. In der Methode run() müssen dann die Aktionen implementiert werden, die periodisch ausgeführt werden sollen.
Um einen Pozess zeitgesteuert ablaufen zu lassen, sind im Einzelnen folgende Schritte erforderlich:
- Es muss eine Unterklasse von TimerTask definiert werden, in der die Methode run() mit den erforderlichen Aktionen überschrieben wird.
- Von dieser Klasse muss ein Exemplar erzeugt werden, aber ohne es bereits als Thread zu starten. Das Starten ist nicht erforderlich, weil sich Timer um das gesamte Thread-Management kümmert.
- Es muss ein Exemplar von Timer erzeugt werden, bei dem die Tasks ablaufen sollen.
- Der Task muss bei dem Timer mit einer der schedule()-Methoden registriert werden.
Den Timer-Methoden zum Registieren von Tasks muss neben einem TimerTask-Exemplar die Periodendauer sowie der Startzeitpunkt übergeben werden. Dieser kann entweder absolut oder als relative Verzögerung gegenüber der aktuellen Systemzeit angegeben werden.
Für die Einstellung der Periodendauer zwischen zwei Ausführungen bestehen ebenfalls zwei Alternativen:Mit Ausführung ist hier eine komplette Ausführung der run()-Methode gemeint.
- Die Periode wird als Zeitspanne zwischen dem Ende einer Ausführung bis zum Beginn der nächsten Ausführung interpretiert (schedule()-Methoden). Das heißt, dass es zwischen den Ausführungszeiten eines Tasks einen stets gleichlangen »Zwischenraum« gibt, unabhängig davon, wie lange die Ausführung dauert.
- Alternativ können die Tasks genau im Abstand des Zeitintervalls gestartet werden, und zwar unabhängig davon, ob während der Ausführung Unterbrechungen oder Blockaden wie I/O, Garbage Collection oder das Warten auf Sperren aufgetreten sind (scheduleAtFixedRate()-Methoden). Bei dieser Variante wird der Startzeitpunkt festgelegt. Sollte es bei der Ausführung zu Verzögerungen kommen, ist die Zeitspanne vom Ende bis zum Start der nächsten Ausführung entsprechend kürzer.
Das folgende Beispiel realisiert einen Thread, der von Zeit zu Zeit die Einträge in einer Warteschlange abarbeitet, die dort von einem anderen Thread abgelegt wurden. Das Beispielprogramm startet einen endlos laufenden Produzenten-Thread, der in zufälligen Zeitabständen neue Elemente in die Warteschlange einträgt. Für die Warteschlange wird die Klasse Vector verwendet. Der Produzent trägt mit dessen Methode add() neue Elemente am Ende ein, wohingegen der andere Thread mit remove() die Elemente am Anfang entfernt. Auf diese Weise wird der Vector als FIFO-Speicher benutzt.public void run() { int newValue; while(true) { newValue = counter++; // Neues Element in der Queue eintragen... queue.add(new Integer(newValue)); System.out.println("Put element "+newValue+" into queue."); // ...und warten try { Thread.sleep(random.nextInt(20)*100); } catch(InterruptedException e) { } } }
Ein zweiter Timer-gesteuerter Thread arbeitet die Schlange ab. Die nachfolgend gezeigte Klasse QueueProcessor ist von TimerTask abgeleitet, ermittelt in der run()-Methode die Anzahl der momentan in der Warteschlange stehenden Elemente und liest diese dann aus.public class QueueProcessor extends TimerTask { protected Vector queue; public QueueProcessor(Vector queue) { this.queue = queue; } public void run() { int currentElements = queue.size(); for(int i = 0; i < currentElements; i++) { String s = queue.remove(0).toString(); System.out.println("Processed element "+s+"."); } } }In beiden Threads wird ohne weitere Synchronisationsmaßnahmen auf die Warteschlange zugegriffen, da die Klasse Vector intern synchronisiert ist.
Das Hauptprogramm initialisiert den Vector, ein Timer-Exemplar sowie ein Exemplar von QueueProcessor. Wie bereits erwähnt, wird bei letzterem zwar ein Exemplar erzeugt, dieses wird aber noch nicht als Thread gestartet.Vector queue; Timer timer; TimerTask processor; // Warteschlange, Timer und Prozessor erzeugen queue = new Vector(); timer = new Timer(); processor = new QueueProcessor(queue); // Produzent starten new Thread(new Producer(queue)).start(); // Prozessor-Thread registrieren timer.scheduleAtFixedRate(processor, 5000, 10000);Danach wird der Produzent gestartet und das QueueProcessor-Exemplar beim Timer registriert. Hierzu wird im Beispiel die Methode scheduleAtFixedRate() aufgerufen, die den Task nach einer erstmaligen Verzögerung von fünf Sekunden alle zehn Sekunden startet.
Um einen TimerTask zu deaktivieren, definiert diese Klasse die Methode cancel(). Nachdem sie einmal aufgerufen wurde, wird der Task künftig nicht mehr ausgeführt. Auch Timer verfügt über diese Methode. Hier werden alle Tasks verworfen und anschließend der Timer selbst beendet.Material zum Beispiel
- Quelltexte: